Files
cloud-hypervisor/cloud-hypervisor/tests/integration.rs
Rob Bradford 7d8986aad0 tests: Allow more time for firmware & O_DIRECT tests
Booting the VM on these tests takes longer so allow longer before
timing out the boot.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-04 21:28:33 +00:00

11490 lines
406 KiB
Rust

// Copyright © 2020 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
#![cfg(any(devcli_testenv, clippy))]
#![allow(clippy::undocumented_unsafe_blocks)]
// When enabling the `mshv` feature, we skip quite some tests and
// hence have known dead-code. This annotation silences dead-code
// related warnings for our quality workflow to pass.
#![allow(dead_code)]
use std::fs::{File, OpenOptions, copy};
use std::io::{Read, Seek, Write};
#[cfg(not(feature = "mshv"))]
use std::net::TcpListener;
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::string::String;
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::{fs, thread};
use block::ImageType;
use test_infra::*;
use vmm_sys_util::tempdir::TempDir;
use vmm_sys_util::tempfile::TempFile;
use wait_timeout::ChildExt;
mod common;
use common::tests_wrappers::*;
use common::utils::*;
macro_rules! basic_regular_guest {
($image_name:expr) => {{
let disk_config = UbuntuDiskConfig::new($image_name.to_string());
GuestFactory::new_regular_guest_factory().create_guest(Box::new(disk_config))
}};
}
mod common_parallel {
use std::io::{self, SeekFrom};
#[cfg(not(feature = "mshv"))]
use std::num::NonZeroU32;
use std::process::Command;
use test_infra::GuestFactory;
#[cfg(not(feature = "mshv"))]
use vmm::api::TimeoutStrategy;
use crate::*;
#[test]
#[cfg(target_arch = "x86_64")]
fn test_focal_hypervisor_fw() {
let guest = basic_regular_guest!(FOCAL_IMAGE_NAME)
.with_kernel(fw_path(FwType::RustHypervisorFirmware));
_test_simple_launch(&guest);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_focal_ovmf() {
let guest = basic_regular_guest!(FOCAL_IMAGE_NAME).with_kernel(fw_path(FwType::Ovmf));
_test_simple_launch(&guest);
}
#[test]
fn test_multi_cpu() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_multi_cpu(&guest);
}
#[test]
#[cfg_attr(target_arch = "x86_64", should_panic)]
fn test_cpu_topology_421() {
test_cpu_topology(4, 2, 1, false);
}
#[test]
fn test_cpu_topology_142() {
test_cpu_topology(1, 4, 2, false);
}
#[test]
fn test_cpu_topology_262() {
test_cpu_topology(2, 6, 2, false);
}
#[test]
#[cfg(target_arch = "x86_64")]
#[cfg(not(feature = "mshv"))]
fn test_cpu_physical_bits() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let max_phys_bits: u8 = 36;
let mut child = GuestCommand::new(&guest)
.args(["--cpus", &format!("max_phys_bits={max_phys_bits}")])
.default_memory()
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.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!(
guest
.ssh_command("lscpu | grep \"Address sizes:\" | cut -f 2 -d \":\" | sed \"s# *##\" | cut -f 1 -d \" \"")
.unwrap()
.trim()
.parse::<u8>()
.unwrap_or(max_phys_bits + 1) <= max_phys_bits,
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
fn _test_nested_virtualization(nested: bool) {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config)).with_nested(nested);
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.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();
let expected = if nested { "yes" } else { "no" };
assert_eq!(
guest
.ssh_command("test -c /dev/kvm && echo yes || echo no")
.unwrap()
.trim(),
expected
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_nested_virtualization_on() {
_test_nested_virtualization(true);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_nested_virtualization_off() {
_test_nested_virtualization(false);
}
#[test]
fn test_cpu_affinity() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2);
_test_cpu_affinity(&guest);
}
#[test]
fn test_virtio_queue_affinity() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4);
_test_virtio_queue_affinity(&guest);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_large_vm() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let mut cmd = GuestCommand::new(&guest);
cmd.args(["--cpus", "boot=48"])
.args(["--memory", "size=5120M"])
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args(["--serial", "tty"])
.args(["--console", "off"])
.capture_output()
.default_disks()
.default_net();
let mut child = cmd.spawn().unwrap();
guest.wait_vm_boot().unwrap();
let r = std::panic::catch_unwind(|| {
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 48);
assert_eq!(
guest
.ssh_command("lscpu | grep \"On-line\" | cut -f 2 -d \":\" | sed \"s# *##\"")
.unwrap()
.trim(),
"0-47"
);
assert!(guest.get_total_memory().unwrap_or_default() > 5_000_000);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_huge_memory() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let mut cmd = GuestCommand::new(&guest);
cmd.default_cpus()
.args(["--memory", "size=128G"])
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.capture_output()
.default_disks()
.default_net();
let mut child = cmd.spawn().unwrap();
guest.wait_vm_boot().unwrap();
let r = std::panic::catch_unwind(|| {
assert!(guest.get_total_memory().unwrap_or_default() > 128_000_000);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_power_button() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_power_button(&guest);
}
#[test]
#[cfg(not(feature = "mshv"))] // See #7456
fn test_user_defined_memory_regions() {
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 mut child = GuestCommand::new(&guest)
.default_cpus()
.args(["--memory", "size=0,hotplug_method=virtio-mem"])
.args([
"--memory-zone",
"id=mem0,size=1G,hotplug_size=2G",
"id=mem1,size=1G,shared=on",
"id=mem2,size=1G,host_numa_node=0,hotplug_size=2G",
])
.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();
assert!(guest.get_total_memory().unwrap_or_default() > 2_880_000);
guest.enable_memory_hotplug();
resize_zone_command(&api_socket, "mem0", "3G");
assert!(wait_until(Duration::from_secs(5), || guest
.get_total_memory()
.unwrap_or_default()
> 4_800_000));
resize_zone_command(&api_socket, "mem2", "3G");
assert!(wait_until(Duration::from_secs(5), || guest
.get_total_memory()
.unwrap_or_default()
> 6_720_000));
resize_zone_command(&api_socket, "mem0", "2G");
assert!(wait_until(Duration::from_secs(5), || guest
.get_total_memory()
.unwrap_or_default()
> 5_760_000));
resize_zone_command(&api_socket, "mem2", "2G");
assert!(wait_until(Duration::from_secs(5), || guest
.get_total_memory()
.unwrap_or_default()
> 4_800_000));
guest.reboot_linux(0);
// Check the amount of RAM after reboot
assert!(guest.get_total_memory().unwrap_or_default() > 4_800_000);
assert!(guest.get_total_memory().unwrap_or_default() < 5_760_000);
// Check if we can still resize down to the initial 'boot'size
resize_zone_command(&api_socket, "mem0", "1G");
assert!(wait_until(Duration::from_secs(5), || guest
.get_total_memory()
.unwrap_or_default()
< 4_800_000));
resize_zone_command(&api_socket, "mem2", "1G");
assert!(wait_until(Duration::from_secs(5), || guest
.get_total_memory()
.unwrap_or_default()
< 3_840_000));
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))] // See #7456
fn test_guest_numa_nodes() {
_test_guest_numa_nodes(false);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_iommu_segments() {
let focal_image = FOCAL_IMAGE_NAME.to_string();
let disk_config = UbuntuDiskConfig::new(focal_image);
let guest = Guest::new(Box::new(disk_config));
// 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 api_socket = temp_api_path(&guest.tmp_dir);
let mut cmd = GuestCommand::new(&guest);
cmd.default_cpus()
.args(["--api-socket", &api_socket])
.default_memory()
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args([
"--platform",
&format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS},iommu_segments=[1]"),
])
.default_disks()
.capture_output()
.default_net();
let mut child = cmd.spawn().unwrap();
guest.wait_vm_boot().unwrap();
let r = std::panic::catch_unwind(|| {
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-disk",
Some(
format!(
"path={},id=test0,pci_segment=1,iommu=on",
test_disk_path.as_str()
)
.as_str(),
),
);
assert!(cmd_success);
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"test0\",\"bdf\":\"0001:00:01.0\"}")
);
// Check IOMMU setup
assert!(
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")
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_pci_msi() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_pci_msi(&guest);
}
#[test]
fn test_virtio_net_ctrl_queue() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_virtio_net_ctrl_queue(&guest);
}
#[test]
fn test_pci_multiple_segments() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_pci_multiple_segments(&guest, MAX_NUM_PCI_SEGMENTS, 15u16);
}
#[test]
fn test_pci_multiple_segments_numa_node() {
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 = edk2_path();
// 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());
const TEST_DISK_NODE: u16 = 1;
let mut child = GuestCommand::new(&guest)
.args(["--platform", "num_pci_segments=2"])
.args(["--cpus", "boot=2"])
.args(["--memory", "size=0"])
.args(["--memory-zone", "id=mem0,size=256M", "id=mem1,size=256M"])
.args([
"--numa",
"guest_numa_id=0,cpus=[0],distances=[1@20],memory_zones=mem0,pci_segments=[0]",
"guest_numa_id=1,cpus=[1],distances=[0@20],memory_zones=mem1,pci_segments=[1]",
])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args(["--api-socket", &api_socket])
.capture_output()
.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={TEST_DISK_NODE}").as_str(),
])
.default_net()
.spawn()
.unwrap();
let cmd = "cat /sys/block/vdc/device/../numa_node";
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command(cmd)
.unwrap()
.trim()
.parse::<u16>()
.unwrap_or_default(),
TEST_DISK_NODE
);
// Each PNP0A08 host bridge in the DSDT must expose a unique
// _UID matching its PCI segment id. Linux surfaces the
// evaluated _UID via /sys/bus/acpi/devices/PNP0A08:*/uid.
// This test uses firmware boot on aarch64, so ACPI is
// available on both supported architectures.
let mut uids: Vec<u16> = guest
.ssh_command("cat /sys/bus/acpi/devices/PNP0A08:*/uid")
.unwrap()
.lines()
.filter_map(|l| l.trim().parse::<u16>().ok())
.collect();
uids.sort();
assert_eq!(uids, vec![0u16, 1u16]);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_direct_kernel_boot() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_direct_kernel_boot(&guest);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_direct_kernel_boot_bzimage() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let mut kernel_path = direct_kernel_boot_path();
// Replace the default kernel with the bzImage.
kernel_path.pop();
kernel_path.push("bzImage-x86_64");
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()
.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);
let grep_cmd = "grep -c PCI-MSI /proc/interrupts";
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);
}
#[test]
fn test_virtio_block_io_uring() {
let guest =
make_virtio_block_guest(&GuestFactory::new_regular_guest_factory(), FOCAL_IMAGE_NAME);
_test_virtio_block(&guest, false, true, false, false, ImageType::Raw);
}
#[test]
fn test_virtio_block_aio() {
let guest =
make_virtio_block_guest(&GuestFactory::new_regular_guest_factory(), FOCAL_IMAGE_NAME)
.with_cpu(4);
_test_virtio_block(&guest, true, false, false, false, ImageType::Raw);
}
#[test]
fn test_virtio_block_sync() {
let guest =
make_virtio_block_guest(&GuestFactory::new_regular_guest_factory(), FOCAL_IMAGE_NAME)
.with_cpu(4);
_test_virtio_block(&guest, true, true, false, false, ImageType::Raw);
}
#[test]
fn test_compute_file_checksum_empty() {
let mut reader = io::Cursor::new(vec![]);
let checksum = compute_file_checksum(&mut reader, 0);
assert_eq!(checksum, 5381);
}
#[test]
fn test_compute_file_checksum_small() {
let data = b"hello world";
let mut reader = io::Cursor::new(data);
let checksum = compute_file_checksum(&mut reader, data.len() as u64);
assert_eq!(checksum, 894552257);
}
#[test]
fn test_compute_file_checksum_same_data() {
let data = b"test data 123";
let mut reader1 = io::Cursor::new(data);
let mut reader2 = io::Cursor::new(data);
let checksum1 = compute_file_checksum(&mut reader1, data.len() as u64);
let checksum2 = compute_file_checksum(&mut reader2, data.len() as u64);
assert_eq!(checksum1, checksum2);
}
#[test]
fn test_compute_file_checksum_different_data() {
let data1 = b"data1";
let data2 = b"data2";
let mut reader1 = io::Cursor::new(data1);
let mut reader2 = io::Cursor::new(data2);
let checksum1 = compute_file_checksum(&mut reader1, data1.len() as u64);
let checksum2 = compute_file_checksum(&mut reader2, data2.len() as u64);
assert_ne!(checksum1, checksum2);
}
#[test]
fn test_compute_file_checksum_large_data() {
let size = 20 * 1024 * 1024;
let data = vec![0xABu8; size];
let mut reader = io::Cursor::new(data);
let checksum = compute_file_checksum(&mut reader, size as u64);
// Should only read first 16MB
assert!(checksum != 5381);
// Verify only 16MB was read
let position = reader.position();
assert_eq!(position, 16 * 1024 * 1024);
}
#[test]
fn test_virtio_block_qcow2() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
let guest = GuestFactory::new_regular_guest_factory()
.create_guest(Box::new(disk_config))
.with_cpu(4);
_test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2);
}
#[test]
fn test_virtio_block_qcow2_zlib() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2_ZLIB.to_string());
let guest = GuestFactory::new_regular_guest_factory()
.create_guest(Box::new(disk_config))
.with_cpu(4);
_test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2);
}
#[test]
fn test_virtio_block_qcow2_zstd() {
let guest = make_virtio_block_guest(
&GuestFactory::new_regular_guest_factory(),
JAMMY_IMAGE_NAME_QCOW2_ZSTD,
);
_test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2);
}
#[test]
fn test_virtio_block_qcow2_backing_zstd_file() {
let guest = make_virtio_block_guest(
&GuestFactory::new_regular_guest_factory(),
JAMMY_IMAGE_NAME_QCOW2_BACKING_ZSTD_FILE,
);
_test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2);
}
#[test]
fn test_virtio_block_qcow2_backing_uncompressed_file() {
let guest = make_virtio_block_guest(
&GuestFactory::new_regular_guest_factory(),
JAMMY_IMAGE_NAME_QCOW2_BACKING_UNCOMPRESSED_FILE,
);
_test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2);
}
#[test]
fn test_virtio_block_qcow2_backing_raw_file() {
let guest = make_virtio_block_guest(
&GuestFactory::new_regular_guest_factory(),
JAMMY_IMAGE_NAME_QCOW2_BACKING_RAW_FILE,
);
_test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2);
}
/// Configuration for QCOW2 multiqueue test image setup
enum QcowTestImageConfig {
/// Simple QCOW2 image with given size (e.g., "256M")
Simple(&'static str),
/// QCOW2 overlay with backing file
WithBacking,
}
/// Helper to run QCOW2 multiqueue stress tests with shared setup/teardown.
///
/// Creates a VM with multiple virtio queues on the test disk, then runs the
/// provided test closure. Handles VM lifecycle and consistency checks.
fn run_multiqueue_qcow2_test<F>(image_config: &QcowTestImageConfig, test_fn: F)
where
F: FnOnce(&Guest) + std::panic::UnwindSafe,
{
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
let test_image_path = guest.tmp_dir.as_path().join("test.qcow2");
// Create test image based on configuration and capture backing checksum if applicable
let initial_backing_checksum = match *image_config {
QcowTestImageConfig::Simple(size) => {
Command::new("qemu-img")
.arg("create")
.args(["-f", "qcow2"])
.arg(test_image_path.to_str().unwrap())
.arg(size)
.output()
.expect("Failed to create QCOW2 test image");
None
}
QcowTestImageConfig::WithBacking => {
let backing_path = guest.tmp_dir.as_path().join("backing.qcow2");
Command::new("qemu-img")
.arg("create")
.args(["-f", "qcow2"])
.arg(backing_path.to_str().unwrap())
.arg("256M")
.output()
.expect("Failed to create backing QCOW2");
Command::new("qemu-img")
.arg("create")
.args(["-f", "qcow2"])
.args(["-b", backing_path.to_str().unwrap()])
.args(["-F", "qcow2"])
.arg(test_image_path.to_str().unwrap())
.output()
.expect("Failed to create overlay QCOW2");
compute_backing_checksum(&test_image_path)
}
};
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=8"])
.args(["--memory", "size=1024M"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args([
"--disk",
&format!(
"path={},num_queues=8",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
),
&format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
),
&format!(
"path={},num_queues=8,backing_files={},image_type=qcow2",
test_image_path.to_str().unwrap(),
if initial_backing_checksum.is_some() {
"on"
} else {
"off"
},
),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
test_fn(&guest);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
disk_check_consistency(
guest.disk_config.disk(DiskType::OperatingSystem).unwrap(),
None,
);
disk_check_consistency(&test_image_path, initial_backing_checksum);
}
#[test]
fn test_virtio_block_qcow2_multiqueue_writes() {
run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("256M"), |guest| {
assert_eq!(
guest
.ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
8,
"Expected 8 queues on vdc"
);
guest
.ssh_command("sudo mkfs.ext4 -F /dev/vdc")
.expect("Failed to format disk");
guest
.ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test")
.expect("Failed to mount disk");
guest
.ssh_command(
"for i in $(seq 1 8); do \
sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=32 conv=fsync & \
done; wait",
)
.expect("Failed to write files in parallel");
assert_eq!(
guest
.ssh_command("ls /mnt/test/file* | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
8,
"Expected 8 files to be created"
);
guest
.ssh_command("sudo rm -f /mnt/test/file*")
.expect("Failed to remove files");
// Do another round of heavy parallel I/O
guest
.ssh_command(
"for i in $(seq 1 16); do \
sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=16 conv=fsync & \
done; wait",
)
.expect("Failed to write files in second round");
assert_eq!(
guest
.ssh_command("ls /mnt/test/file* | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
16,
"Expected 16 files after second round"
);
guest
.ssh_command("sudo umount /mnt/test")
.expect("Failed to unmount");
});
}
#[test]
fn test_virtio_block_qcow2_multiqueue_mixed_rw() {
run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("512M"), |guest| {
guest
.ssh_command("sudo mkfs.ext4 -F /dev/vdc")
.expect("Failed to format disk");
guest
.ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test")
.expect("Failed to mount disk");
guest
.ssh_command(
"sudo dd if=/dev/urandom of=/mnt/test/readfile bs=1M count=64 conv=fsync",
)
.expect("Failed to create initial file");
guest
.ssh_command(
"for i in $(seq 1 4); do \
sudo dd if=/mnt/test/readfile of=/dev/null bs=64K & \
sudo dd if=/dev/urandom of=/mnt/test/writefile$i bs=1M count=32 conv=fsync & \
done; wait",
)
.expect("Failed mixed read/write workload");
assert_eq!(
guest
.ssh_command("ls /mnt/test/writefile* | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
4,
"Expected 4 write files"
);
guest
.ssh_command(
"for i in $(seq 1 4); do \
sudo dd if=/mnt/test/writefile$i of=/dev/null bs=64K & \
sudo dd if=/dev/urandom of=/mnt/test/newfile$i bs=1M count=16 conv=fsync & \
done; wait",
)
.expect("Failed second mixed workload");
guest
.ssh_command("sudo umount /mnt/test")
.expect("Failed to unmount");
});
}
#[test]
fn test_virtio_block_qcow2_multiqueue_backing() {
run_multiqueue_qcow2_test(&QcowTestImageConfig::WithBacking, |guest| {
guest
.ssh_command("sudo mkfs.ext4 -F /dev/vdc")
.expect("Failed to format disk");
guest
.ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test")
.expect("Failed to mount disk");
guest
.ssh_command(
"for i in $(seq 1 8); do \
sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=16 conv=fsync & \
done; wait",
)
.expect("Failed to write files");
guest
.ssh_command(
"for i in $(seq 1 8); do \
sudo dd if=/mnt/test/file$i of=/dev/null bs=64K & \
sudo dd if=/dev/urandom of=/mnt/test/new$i bs=1M count=8 conv=fsync & \
done; wait",
)
.expect("Failed mixed backing/overlay workload");
assert_eq!(
guest
.ssh_command("ls /mnt/test/new* | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
8,
"Expected 8 new files"
);
guest
.ssh_command("sudo umount /mnt/test")
.expect("Failed to unmount");
});
}
#[test]
fn test_virtio_block_qcow2_multiqueue_random_4k() {
run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("256M"), |guest| {
guest
.ssh_command(
"for i in $(seq 1 8); do \
sudo dd if=/dev/urandom of=/dev/vdc bs=4K count=1000 seek=$((RANDOM % 60000)) conv=notrunc & \
done; wait",
)
.expect("Failed random 4K writes round 1");
guest
.ssh_command(
"for i in $(seq 1 8); do \
sudo dd if=/dev/urandom of=/dev/vdc bs=4K count=1000 seek=$((RANDOM % 60000)) conv=notrunc & \
done; wait",
)
.expect("Failed random 4K writes round 2");
guest
.ssh_command(
"for i in $(seq 1 4); do \
sudo dd if=/dev/vdc of=/dev/null bs=4K count=500 skip=$((RANDOM % 60000)) & \
sudo dd if=/dev/urandom of=/dev/vdc bs=4K count=500 seek=$((RANDOM % 60000)) conv=notrunc & \
done; wait",
)
.expect("Failed mixed random I/O");
});
}
#[test]
fn test_virtio_block_qcow2_multiqueue_fsync() {
run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("256M"), |guest| {
guest
.ssh_command("sudo mkfs.ext4 -F /dev/vdc")
.expect("Failed to format disk");
guest
.ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test")
.expect("Failed to mount disk");
guest
.ssh_command(
"for i in $(seq 1 8); do \
(for j in $(seq 1 100); do \
echo \"data$j\" | sudo tee /mnt/test/file${i}_$j > /dev/null && sudo sync; \
done) & \
done; wait",
)
.expect("Failed fsync storm round 1");
assert_eq!(
guest
.ssh_command("ls /mnt/test/file* | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
800,
"Expected 800 files (8 processes x 100 files)"
);
guest
.ssh_command(
"for i in $(seq 1 8); do \
(for j in $(seq 1 50); do \
sudo dd if=/dev/urandom of=/mnt/test/dd${i}_$j bs=4K count=1 conv=fsync 2>/dev/null; \
done) & \
done; wait",
)
.expect("Failed fsync storm round 2");
guest
.ssh_command("sudo umount /mnt/test")
.expect("Failed to unmount");
});
}
#[test]
fn test_virtio_block_qcow2_multiqueue_metadata() {
run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("256M"), |guest| {
guest
.ssh_command("sudo mkfs.ext4 -F /dev/vdc")
.expect("Failed to format disk");
guest
.ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test")
.expect("Failed to mount disk");
guest
.ssh_command(
"for i in $(seq 1 8); do \
(for j in $(seq 1 50); do \
sudo mkdir -p /mnt/test/dir$i/subdir$j; \
done) & \
done; wait",
)
.expect("Failed parallel mkdir");
let dir_count: u32 = guest
.ssh_command("find /mnt/test -type d | wc -l")
.expect("Failed to count directories")
.trim()
.parse()
.unwrap_or(0);
assert!(
dir_count >= 400,
"Expected at least 400 directories, got {dir_count}"
);
guest
.ssh_command(
"for i in $(seq 1 8); do \
(for j in $(seq 1 100); do \
sudo touch /mnt/test/dir$i/file$j; \
done) & \
done; wait",
)
.expect("Failed parallel touch");
let file_count: u32 = guest
.ssh_command("find /mnt/test -type f | wc -l")
.expect("Failed to count files")
.trim()
.parse()
.unwrap_or(0);
assert!(
file_count >= 400,
"Expected at least 400 files, got {file_count}"
);
guest
.ssh_command(
"for i in $(seq 1 4); do \
sudo rm -rf /mnt/test/dir$i & \
(for j in $(seq 1 50); do \
sudo touch /mnt/test/newfile${i}_$j; \
done) & \
done; wait",
)
.expect("Failed parallel rm + touch");
guest
.ssh_command(
"for i in $(seq 5 8); do \
(for j in $(seq 1 25); do \
sudo mv /mnt/test/dir$i/file$j /mnt/test/dir$i/renamed$j 2>/dev/null || true; \
done) & \
done; wait",
)
.expect("Failed parallel rename");
guest
.ssh_command("sync && sudo umount /mnt/test")
.expect("Failed to unmount");
});
}
#[test]
fn test_virtio_block_qcow2_multiqueue_discard_mount() {
run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("256M"), |guest| {
guest
.ssh_command("sudo mkfs.ext4 -F /dev/vdc")
.expect("Failed to format disk");
// Mount with discard option to enable automatic TRIM/DISCARD
guest
.ssh_command("sudo mkdir -p /mnt/test && sudo mount -o discard /dev/vdc /mnt/test")
.expect("Failed to mount disk with discard option");
guest
.ssh_command(
"for i in $(seq 1 4); do \n\
sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=32 conv=fsync & \n\
done; wait",
)
.expect("Failed to write files in parallel");
assert_eq!(
guest
.ssh_command("ls /mnt/test/file* | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
4,
"Expected 4 files to be created"
);
guest
.ssh_command("sudo rm -f /mnt/test/file*")
.expect("Failed to remove files");
guest
.ssh_command("sudo fstrim -v /mnt/test")
.expect("fstrim failed - DISCARD not working");
guest
.ssh_command(
"for i in $(seq 1 8); do \n\
sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=16 conv=fsync & \n\
done; wait",
)
.expect("Failed to write files in second round");
assert_eq!(
guest
.ssh_command("ls /mnt/test/file* | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
8,
"Expected 8 files after second round"
);
guest
.ssh_command("sudo umount /mnt/test")
.expect("Failed to unmount");
});
}
#[test]
fn test_virtio_block_qcow2_multiqueue_wide_writes() {
run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("1G"), |guest| {
// Scattered write pattern - write to widely separated offsets in parallel.
// This should initiate many L2 table allocations simultaneously across different queues.
guest
.ssh_command(
"for i in $(seq 0 7); do \n\
offset=$((i * 128)) \n\
sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=16 seek=$offset conv=notrunc,fsync & \n\
done; wait",
)
.expect("Failed to write sparse pattern in parallel");
// Write known patterns to the same sparse locations
guest
.ssh_command(
"for i in $(seq 0 7); do \n\
offset=$((i * 128)) \n\
sudo dd if=/dev/zero of=/dev/vdc bs=1M count=8 seek=$offset conv=notrunc,fsync & \n\
done; wait",
)
.expect("Failed second sparse write pattern");
// Even more aggressive sparse writes with smaller chunks but more of them
guest
.ssh_command(
"for i in $(seq 0 15); do \n\
offset=$((i * 64)) \n\
sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=2 seek=$offset conv=notrunc,fsync & \n\
done; wait",
)
.expect("Failed third sparse write pattern");
guest
.ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M count=64")
.expect("Failed to read back data after sparse writes");
});
}
#[test]
fn test_virtio_block_qcow2_multiqueue_discard_stress() {
run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("512M"), |guest| {
guest
.ssh_command("sudo mkfs.ext4 -F /dev/vdc")
.expect("Failed to format disk");
guest
.ssh_command("sudo mkdir -p /mnt/test && sudo mount -o discard /dev/vdc /mnt/test")
.expect("Failed to mount disk with discard option");
// Round 1: Start background writes while simultaneously doing DISCARD operations
// This stresses refcount table locking - writes increment refs, discard decrements
guest
.ssh_command(
"for i in $(seq 1 4); do \n\
sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=32 & \n\
done",
)
.expect("Failed to start background writes");
guest
.ssh_command(
"for i in $(seq 5 8); do \n\
sudo dd if=/dev/urandom of=/mnt/test/temp$i bs=1M count=16 conv=fsync \n\
sudo rm -f /mnt/test/temp$i & \n\
done; \n\
wait; \n\
sudo fstrim -v /mnt/test",
)
.expect("Failed to do parallel write-delete-discard");
guest
.ssh_command("wait")
.expect("Failed to wait for background writes");
assert_eq!(
guest
.ssh_command("ls /mnt/test/file* 2>/dev/null | wc -l")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
4,
"Expected 4 files after round 1"
);
// Round 2: More aggressive - 8 parallel writes with simultaneous blkdiscard on raw device
guest
.ssh_command("sudo umount /mnt/test")
.expect("Failed to unmount");
guest
.ssh_command(
"for i in $(seq 0 7); do \n\
offset=$((i * 64)) \n\
sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=4 seek=$offset conv=notrunc,fsync & \n\
done; wait",
)
.expect("Failed sparse writes");
// Now discard half the regions while writing to the other half
guest
.ssh_command(
"for i in $(seq 0 3); do \n\
offset=$((i * 64 * 1024 * 1024)) \n\
sudo blkdiscard -o $offset -l $((4 * 1024 * 1024)) /dev/vdc & \n\
done; \n\
for i in $(seq 4 7); do \n\
offset=$((i * 64)) \n\
sudo dd if=/dev/zero of=/dev/vdc bs=1M count=4 seek=$offset conv=notrunc,fsync & \n\
done; wait",
)
.expect("Failed parallel discard and write stress test");
guest
.ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M count=128")
.expect("Failed to read back data after discard stress");
});
}
#[test]
fn test_virtio_block_qcow2_uefi_direct_io() {
// Regression test for #8007.
// Place the QCOW2 OS image on a 4096 byte sector filesystem so
// O_DIRECT forces 4096 byte alignment on all I/O buffers.
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = edk2_path();
let mut workloads_path = dirs::home_dir().unwrap();
workloads_path.push("workloads");
let img_dir = TempDir::new_in(workloads_path.as_path()).unwrap();
let fs_img_path = img_dir.as_path().join("fs_4ksec.img");
assert!(
exec_host_command_output(&format!("truncate -s 4G {}", fs_img_path.to_str().unwrap()))
.status
.success(),
"truncate failed"
);
let loop_dev_path = create_loop_device(fs_img_path.to_str().unwrap(), 4096, 5);
assert!(
exec_host_command_output(&format!("mkfs.ext4 -q {loop_dev_path}"))
.status
.success(),
"mkfs.ext4 failed"
);
let mnt_dir = img_dir.as_path().join("mnt");
fs::create_dir_all(&mnt_dir).unwrap();
assert!(
exec_host_command_output(&format!(
"mount {} {}",
&loop_dev_path,
mnt_dir.to_str().unwrap()
))
.status
.success(),
"mount failed"
);
let src_qcow2 = guest.disk_config.disk(DiskType::OperatingSystem).unwrap();
let dest_qcow2 = mnt_dir.join("os.qcow2");
assert!(
exec_host_command_output(&format!(
"cp {} {}",
&src_qcow2,
dest_qcow2.to_str().unwrap()
))
.status
.success(),
"cp failed"
);
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args([
"--disk",
&format!(
"path={},direct=on,image_type=qcow2",
dest_qcow2.to_str().unwrap()
),
&format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot_custom_timeout(180).unwrap();
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
let _ = exec_host_command_output(&format!("umount {}", mnt_dir.to_str().unwrap()));
let _ = exec_host_command_output(&format!("losetup -d {loop_dev_path}"));
handle_child_output(r, &output);
}
#[test]
fn test_virtio_block_qcow2_dirty_bit_unclean_shutdown() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
let test_image_path = guest.tmp_dir.as_path().join("test-dirty.qcow2");
let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap();
copy(original_image, &test_image_path).expect("Failed to copy qcow2 image");
assert_eq!(
check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"),
Some(false),
"Image should start with dirty bit cleared"
);
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={}", test_image_path.to_str().unwrap()),
&format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"),
Some(true),
"Dirty bit should be set while VM is running"
);
});
if r.is_err() {
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
return;
}
// Simulate unclean shutdown with SIGKILL
let _ = unsafe { libc::kill(child.id() as i32, libc::SIGKILL) };
let _ = child.wait();
assert_eq!(
check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"),
Some(true),
"Dirty bit should remain set after unclean shutdown"
);
}
#[test]
fn test_virtio_block_qcow2_dirty_bit_clean_shutdown() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
let test_image_path = guest.tmp_dir.as_path().join("test-dirty.qcow2");
let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap();
copy(original_image, &test_image_path).expect("Failed to copy qcow2 image");
assert_eq!(
check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"),
Some(false),
"Image should start with dirty bit cleared"
);
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={}", test_image_path.to_str().unwrap()),
&format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"),
Some(true),
"Dirty bit should be set while VM is running"
);
});
// Clean shutdown using SIGTERM
kill_child(&mut child);
if r.is_err() {
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
return;
}
let _ = child.wait();
disk_check_consistency(&test_image_path, None);
}
#[test]
fn test_virtio_block_qcow2_corrupt_bit_rejected_for_write() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
let test_image_path = guest.tmp_dir.as_path().join("test-corrupt.qcow2");
let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap();
copy(original_image, &test_image_path).expect("Failed to copy qcow2 image");
assert_eq!(
check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"),
Some(false),
"Image should start with corrupt bit cleared"
);
set_corrupt_flag(&test_image_path, true).expect("Failed to set corrupt flag");
assert_eq!(
check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"),
Some(true),
"Corrupt bit should be set"
);
let 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={}", test_image_path.to_str().unwrap()),
&format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(
!output.status.success(),
"VM should fail to start with corrupt disk image"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("corrupt") || stderr.contains("Corrupt"),
"Error message should mention corruption: {stderr}"
);
}
#[test]
fn test_virtio_block_qcow2_corrupt_bit_allowed_readonly() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
let test_image_path = guest.tmp_dir.as_path().join("test-corrupt-ro.qcow2");
let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap();
copy(original_image, &test_image_path).expect("Failed to copy qcow2 image");
set_corrupt_flag(&test_image_path, true).expect("Failed to set corrupt flag");
assert_eq!(
check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"),
Some(true),
"Corrupt bit should be set"
);
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={},readonly=on", test_image_path.to_str().unwrap()),
&format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
thread::sleep(Duration::from_secs(5));
match child.try_wait() {
Ok(Some(status)) => {
let output = child.wait_with_output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
panic!(
"VM should not have exited when opening corrupt image as readonly. Exit status: {status}, stderr: {stderr}"
);
}
Ok(None) => {
// VM is still running as expected
}
Err(e) => {
panic!("Error checking process status: {e}");
}
}
let _ = unsafe { libc::kill(child.id() as i32, libc::SIGKILL) };
let output = child.wait_with_output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("QCOW2 image is marked corrupt, opening read-only"),
"Expected warning about corrupt image being opened read-only. stderr: {stderr}"
);
assert_eq!(
check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"),
Some(true),
"Corrupt bit should remain set for read-only access"
);
}
#[test]
fn test_virtio_block_vhd() {
let mut workload_path = dirs::home_dir().unwrap();
workload_path.push("workloads");
let mut raw_file_path = workload_path.clone();
let mut vhd_file_path = workload_path;
raw_file_path.push(FOCAL_IMAGE_NAME);
vhd_file_path.push(FOCAL_IMAGE_NAME_VHD);
// Generate VHD file from RAW file
std::process::Command::new("qemu-img")
.arg("convert")
.arg("-p")
.args(["-f", "raw"])
.args(["-O", "vpc"])
.args(["-o", "subformat=fixed"])
.arg(raw_file_path.to_str().unwrap())
.arg(vhd_file_path.to_str().unwrap())
.output()
.expect("Expect generating VHD image from RAW image");
let guest = make_virtio_block_guest(
&GuestFactory::new_regular_guest_factory(),
FOCAL_IMAGE_NAME_VHD,
);
_test_virtio_block(&guest, false, false, false, false, ImageType::FixedVhd);
}
#[test]
fn test_virtio_block_vhdx() {
let mut workload_path = dirs::home_dir().unwrap();
workload_path.push("workloads");
let mut raw_file_path = workload_path.clone();
let mut vhdx_file_path = workload_path;
raw_file_path.push(FOCAL_IMAGE_NAME);
vhdx_file_path.push(FOCAL_IMAGE_NAME_VHDX);
// Generate dynamic VHDX file from RAW file
std::process::Command::new("qemu-img")
.arg("convert")
.arg("-p")
.args(["-f", "raw"])
.args(["-O", "vhdx"])
.arg(raw_file_path.to_str().unwrap())
.arg(vhdx_file_path.to_str().unwrap())
.output()
.expect("Expect generating dynamic VHDx image from RAW image");
let guest = make_virtio_block_guest(
&GuestFactory::new_regular_guest_factory(),
FOCAL_IMAGE_NAME_VHDX,
);
_test_virtio_block(&guest, false, false, true, false, ImageType::Vhdx);
}
#[test]
fn test_virtio_block_dynamic_vhdx_expand() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_virtio_block_dynamic_vhdx_expand(&guest);
}
#[test]
fn test_virtio_block_direct_and_firmware() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
// The OS disk must be copied to a location that is not backed by
// tmpfs, otherwise the syscall openat(2) with O_DIRECT simply fails
// with EINVAL because tmpfs doesn't support this flag.
let mut workloads_path = dirs::home_dir().unwrap();
workloads_path.push("workloads");
let os_dir = TempDir::new_in(workloads_path.as_path()).unwrap();
let mut os_path = os_dir.as_path().to_path_buf();
os_path.push("osdisk.img");
rate_limited_copy(
guest.disk_config.disk(DiskType::OperatingSystem).unwrap(),
os_path.as_path(),
)
.expect("copying of OS disk failed");
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()])
.args([
"--disk",
format!("path={},direct=on", os_path.as_path().to_str().unwrap()).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_custom_timeout(180).unwrap();
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_vhost_user_net_default() {
test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, false, false);
}
#[test]
fn test_vhost_user_net_named_tap() {
test_vhost_user_net(
Some("mytap0"),
2,
&prepare_vhost_user_net_daemon,
false,
false,
);
}
#[test]
fn test_vhost_user_net_existing_tap() {
test_vhost_user_net(
Some("vunet-tap0"),
2,
&prepare_vhost_user_net_daemon,
false,
false,
);
}
#[test]
fn test_vhost_user_net_multiple_queues() {
test_vhost_user_net(None, 4, &prepare_vhost_user_net_daemon, false, false);
}
#[test]
fn test_vhost_user_net_tap_multiple_queues() {
test_vhost_user_net(
Some("vunet-tap1"),
4,
&prepare_vhost_user_net_daemon,
false,
false,
);
}
#[test]
fn test_vhost_user_net_host_mac() {
test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, true, false);
}
#[test]
fn test_vhost_user_net_client_mode() {
test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, false, true);
}
#[test]
#[cfg(not(target_arch = "aarch64"))]
fn test_vhost_user_blk_default() {
test_vhost_user_blk(2, false, false, Some(&prepare_vubd));
}
#[test]
#[cfg(not(target_arch = "aarch64"))]
fn test_vhost_user_blk_readonly() {
test_vhost_user_blk(1, true, false, Some(&prepare_vubd));
}
#[test]
#[cfg(not(target_arch = "aarch64"))]
fn test_vhost_user_blk_direct() {
test_vhost_user_blk(1, false, true, Some(&prepare_vubd));
}
#[test]
fn test_boot_from_vhost_user_blk_default() {
test_boot_from_vhost_user_blk(1, false, false, Some(&prepare_vubd));
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_split_irqchip() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_split_irqchip(&guest);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_dmi_serial_number() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_dmi_serial_number(&guest);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_dmi_uuid() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_dmi_uuid(&guest);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_dmi_oem_strings() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_dmi_oem_strings(&guest);
}
#[test]
fn test_virtio_fs() {
_test_virtio_fs(&prepare_virtiofsd, false, false, None);
}
#[test]
fn test_virtio_fs_hotplug() {
_test_virtio_fs(&prepare_virtiofsd, true, false, None);
}
#[test]
fn test_virtio_fs_multi_segment_hotplug() {
_test_virtio_fs(&prepare_virtiofsd, true, false, Some(15));
}
#[test]
fn test_virtio_fs_multi_segment() {
_test_virtio_fs(&prepare_virtiofsd, false, false, Some(15));
}
#[test]
fn test_generic_vhost_user() {
_test_virtio_fs(&prepare_virtiofsd, false, true, None);
}
#[test]
fn test_generic_vhost_user_hotplug() {
_test_virtio_fs(&prepare_virtiofsd, true, true, None);
}
#[test]
fn test_generic_vhost_user_multi_segment_hotplug() {
_test_virtio_fs(&prepare_virtiofsd, true, true, Some(15));
}
#[test]
fn test_generic_vhost_user_multi_segment() {
_test_virtio_fs(&prepare_virtiofsd, false, true, Some(15));
}
#[test]
fn test_virtio_pmem_discard_writes() {
test_virtio_pmem(true, false);
}
#[test]
fn test_virtio_pmem_with_size() {
test_virtio_pmem(true, true);
}
#[test]
fn test_boot_from_virtio_pmem() {
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 mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args([
"--disk",
format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
])
.default_net()
.args([
"--pmem",
format!(
"file={},size={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap(),
fs::metadata(guest.disk_config.disk(DiskType::OperatingSystem).unwrap())
.unwrap()
.len()
)
.as_str(),
])
.args([
"--cmdline",
DIRECT_KERNEL_BOOT_CMDLINE
.replace("vda1", "pmem0p1")
.as_str(),
])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Simple checks to validate the VM booted properly
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);
}
#[test]
fn test_multiple_network_interfaces() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_multiple_network_interfaces(&guest);
}
#[test]
#[cfg(target_arch = "aarch64")]
fn test_pmu_on() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.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();
// Test that PMU exists.
assert_eq!(
guest
.ssh_command(GREP_PMU_IRQ_CMD)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_serial_off() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_serial_off(&guest);
}
#[test]
fn test_serial_null() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let mut cmd = GuestCommand::new(&guest);
#[cfg(target_arch = "x86_64")]
let console_str: &str = "console=ttyS0";
#[cfg(target_arch = "aarch64")]
let console_str: &str = "console=ttyAMA0";
cmd.default_cpus()
.default_memory()
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args([
"--cmdline",
DIRECT_KERNEL_BOOT_CMDLINE
.replace("console=hvc0", console_str)
.as_str(),
])
.default_disks()
.default_net()
.args(["--serial", "null"])
.args(["--console", "off"])
.capture_output();
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Test that there is a ttyS0
assert_eq!(
guest
.ssh_command(GREP_SERIAL_IRQ_CMD)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
});
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(CONSOLE_TEST_STRING));
});
handle_child_output(r, &output);
}
#[test]
fn test_serial_tty() {
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();
#[cfg(target_arch = "x86_64")]
let console_str: &str = "console=ttyS0";
#[cfg(target_arch = "aarch64")]
let console_str: &str = "console=ttyAMA0";
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args([
"--cmdline",
DIRECT_KERNEL_BOOT_CMDLINE
.replace("console=hvc0", console_str)
.as_str(),
])
.default_disks()
.default_net()
.args(["--serial", "tty"])
.args(["--console", "off"])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Test that there is a ttyS0
assert_eq!(
guest
.ssh_command(GREP_SERIAL_IRQ_CMD)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
});
// This sleep is needed to wait for the login prompt
thread::sleep(std::time::Duration::new(2, 0));
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(CONSOLE_TEST_STRING));
});
handle_child_output(r, &output);
}
#[test]
fn test_serial_file() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let serial_path = guest.tmp_dir.as_path().join("serial-output");
#[cfg(target_arch = "x86_64")]
let console_str: &str = "console=ttyS0";
#[cfg(target_arch = "aarch64")]
let console_str: &str = "console=ttyAMA0";
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args([
"--cmdline",
DIRECT_KERNEL_BOOT_CMDLINE
.replace("console=hvc0", console_str)
.as_str(),
])
.default_disks()
.default_net()
.args([
"--serial",
format!("file={}", serial_path.to_str().unwrap()).as_str(),
])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Test that there is a ttyS0
assert_eq!(
guest
.ssh_command(GREP_SERIAL_IRQ_CMD)
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
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();
handle_child_output(r, &output);
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(serial_path).unwrap();
let mut buf = String::new();
f.read_to_string(&mut buf).unwrap();
assert!(buf.contains(CONSOLE_TEST_STRING));
});
handle_child_output(r, &output);
}
#[test]
fn test_pty_interaction() {
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 serial_option = if cfg!(target_arch = "x86_64") {
" console=ttyS0"
} else {
" console=ttyAMA0"
};
let cmdline = DIRECT_KERNEL_BOOT_CMDLINE.to_owned() + serial_option;
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args(["--cmdline", &cmdline])
.default_disks()
.default_net()
.args(["--serial", "null"])
.args(["--console", "pty"])
.args(["--api-socket", &api_socket])
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Get pty fd for console
let console_path = get_pty_path(&api_socket, "console");
_test_pty_interaction(console_path);
guest.ssh_command("sudo shutdown -h now").unwrap();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(20));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
let r = std::panic::catch_unwind(|| {
// Check that the cloud-hypervisor binary actually terminated
assert!(output.status.success());
});
handle_child_output(r, &output);
}
#[test]
fn test_serial_socket_interaction() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let serial_socket = guest.tmp_dir.as_path().join("serial.socket");
let serial_socket_pty = guest.tmp_dir.as_path().join("serial.pty");
let serial_option = if cfg!(target_arch = "x86_64") {
" console=ttyS0"
} else {
" console=ttyAMA0"
};
let cmdline = DIRECT_KERNEL_BOOT_CMDLINE.to_owned() + serial_option;
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args(["--cmdline", &cmdline])
.default_disks()
.default_net()
.args(["--console", "null"])
.args([
"--serial",
format!("socket={}", serial_socket.to_str().unwrap()).as_str(),
])
.spawn()
.unwrap();
let _ = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
});
let mut socat_command = Command::new("socat");
let socat_args = [
&format!("pty,link={},raw", serial_socket_pty.display()),
&format!("UNIX-CONNECT:{}", serial_socket.display()),
];
socat_command.args(socat_args);
let mut socat_child = socat_command.spawn().unwrap();
thread::sleep(std::time::Duration::new(1, 0));
let _ = std::panic::catch_unwind(|| {
_test_pty_interaction(serial_socket_pty);
});
let _ = socat_child.kill();
let _ = socat_child.wait();
let r = std::panic::catch_unwind(|| {
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();
handle_child_output(r, &output);
let r = std::panic::catch_unwind(|| {
// Check that the cloud-hypervisor binary actually terminated
if !output.status.success() {
panic!(
"Cloud Hypervisor process failed to terminate gracefully: {:?}",
output.status
);
}
});
handle_child_output(r, &output);
}
#[test]
fn test_virtio_console() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_virtio_console(&guest);
}
#[test]
fn test_console_file() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_console_file(&guest);
}
#[test]
#[cfg(target_arch = "x86_64")]
#[cfg(not(feature = "mshv"))]
// The VFIO integration test starts cloud-hypervisor guest with 3 TAP
// backed networking interfaces, bound through a simple bridge on the host.
// So if the nested cloud-hypervisor succeeds in getting a directly
// assigned interface from its cloud-hypervisor host, we should be able to
// ssh into it, and verify that it's running with the right kernel command
// line (We tag the command line from cloud-hypervisor for that purpose).
// The third device is added to validate that hotplug works correctly since
// it is being added to the L2 VM through hotplugging mechanism.
// Also, we pass-through a virtio-blk device to the L2 VM to test the 32-bit
// vfio device support
fn test_vfio() {
setup_vfio_network_interfaces();
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new_from_ip_range(Box::new(disk_config), "172.18", 0);
let mut workload_path = dirs::home_dir().unwrap();
workload_path.push("workloads");
let kernel_path = direct_kernel_boot_path();
let mut vfio_path = workload_path.clone();
vfio_path.push("vfio");
let mut cloud_init_vfio_base_path = vfio_path.clone();
cloud_init_vfio_base_path.push("cloudinit.img");
// We copy our cloudinit into the vfio mount point, for the nested
// cloud-hypervisor guest to use.
rate_limited_copy(
guest.disk_config.disk(DiskType::CloudInit).unwrap(),
&cloud_init_vfio_base_path,
)
.expect("copying of cloud-init disk failed");
let mut vfio_disk_path = workload_path.clone();
vfio_disk_path.push("vfio.img");
// Create the vfio disk image
let output = Command::new("mkfs.ext4")
.arg("-d")
.arg(vfio_path.to_str().unwrap())
.arg(vfio_disk_path.to_str().unwrap())
.arg("2g")
.output()
.unwrap();
if !output.status.success() {
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
panic!("mkfs.ext4 command generated an error");
}
let mut blk_file_path = workload_path;
blk_file_path.push("blk.img");
let vfio_tap0 = "vfio-tap0";
let vfio_tap1 = "vfio-tap1";
let vfio_tap2 = "vfio-tap2";
let vfio_tap3 = "vfio-tap3";
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=4"])
.args(["--memory", "size=2G,hugepages=on,shared=on"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.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={},image_type=raw", vfio_disk_path.to_str().unwrap()).as_str(),
format!("path={},iommu=on,readonly=true", blk_file_path.to_str().unwrap()).as_str(),
])
.args([
"--cmdline",
format!(
"{DIRECT_KERNEL_BOOT_CMDLINE} kvm-intel.nested=1 vfio_iommu_type1.allow_unsafe_interrupts"
)
.as_str(),
])
.args([
"--net",
format!("tap={},mac={}", vfio_tap0, guest.network.guest_mac0).as_str(),
format!(
"tap={},mac={},iommu=on",
vfio_tap1, guest.network.l2_guest_mac1
)
.as_str(),
format!(
"tap={},mac={},iommu=on",
vfio_tap2, guest.network.l2_guest_mac2
)
.as_str(),
format!(
"tap={},mac={},iommu=on",
vfio_tap3, guest.network.l2_guest_mac3
)
.as_str(),
])
.capture_output()
.spawn()
.unwrap();
guest.wait_for_ssh(Duration::from_secs(30)).unwrap();
let r = std::panic::catch_unwind(|| {
guest.ssh_command_l1("sudo systemctl start vfio").unwrap();
let auth = PasswordAuth {
username: String::from("cloud"),
password: String::from("cloud123"),
};
wait_for_ssh(
"true",
&auth,
&guest.network.l2_guest_ip1,
Duration::from_secs(120),
)
.unwrap();
wait_for_ssh(
"true",
&auth,
&guest.network.l2_guest_ip2,
Duration::from_secs(120),
)
.unwrap();
// We booted our cloud hypervisor L2 guest with a "VFIOTAG" tag
// added to its kernel command line.
// Let's ssh into it and verify that it's there. If it is it means
// we're in the right guest (The L2 one) because the QEMU L1 guest
// does not have this command line tag.
assert!(check_matched_lines_count(
guest.ssh_command_l2_1("cat /proc/cmdline").unwrap().trim(),
&["VFIOTAG"],
1
));
// Let's also verify from the second virtio-net device passed to
// the L2 VM.
assert!(check_matched_lines_count(
guest.ssh_command_l2_2("cat /proc/cmdline").unwrap().trim(),
&["VFIOTAG"],
1
));
// Check the amount of PCI devices appearing in L2 VM.
assert!(check_lines_count(
guest
.ssh_command_l2_1("ls /sys/bus/pci/devices")
.unwrap()
.trim(),
8
));
// Check both if /dev/vdc exists and if the block size is 16M in L2 VM
assert!(check_matched_lines_count(
guest.ssh_command_l2_1("lsblk").unwrap().trim(),
&["vdc", "16M"],
1
));
// Hotplug an extra virtio-net device through L2 VM.
guest
.ssh_command_l1(
"echo 0000:00:09.0 | sudo tee /sys/bus/pci/devices/0000:00:09.0/driver/unbind",
)
.unwrap();
guest
.ssh_command_l1("echo 0000:00:09.0 | sudo tee /sys/bus/pci/drivers/vfio-pci/bind")
.unwrap();
let vfio_hotplug_output = guest
.ssh_command_l1(
"sudo /mnt/ch-remote \
--api-socket=/tmp/ch_api.sock \
add-device path=/sys/bus/pci/devices/0000:00:09.0,id=vfio123",
)
.unwrap();
assert!(check_matched_lines_count(
vfio_hotplug_output.trim(),
&["{\"id\":\"vfio123\",\"bdf\":\"0000:00:08.0\"}"],
1
));
wait_for_ssh(
"true",
&auth,
&guest.network.l2_guest_ip3,
Duration::from_secs(10),
)
.unwrap();
assert!(wait_until(Duration::from_secs(10), || {
guest
.ssh_command_l2_1("ls /sys/bus/pci/devices")
.is_ok_and(|output| check_lines_count(output.trim(), 9))
}));
// Let's also verify from the third virtio-net device passed to
// the L2 VM. This third device has been hotplugged through the L2
// VM, so this is our way to validate hotplug works for VFIO PCI.
assert!(check_matched_lines_count(
guest.ssh_command_l2_3("cat /proc/cmdline").unwrap().trim(),
&["VFIOTAG"],
1
));
// Check the amount of PCI devices appearing in L2 VM.
// There should be one more device than before, raising the count
// up to 9 PCI devices.
assert!(check_lines_count(
guest
.ssh_command_l2_1("ls /sys/bus/pci/devices")
.unwrap()
.trim(),
9
));
// Let's now verify that we can correctly remove the virtio-net
// device through the "remove-device" command responsible for
// unplugging VFIO devices.
guest
.ssh_command_l1(
"sudo /mnt/ch-remote \
--api-socket=/tmp/ch_api.sock \
remove-device vfio123",
)
.unwrap();
assert!(wait_until(Duration::from_secs(10), || {
guest
.ssh_command_l2_1("ls /sys/bus/pci/devices")
.is_ok_and(|output| check_lines_count(output.trim(), 8))
}));
// Check the amount of PCI devices appearing in L2 VM is back down
// to 8 devices.
assert!(check_lines_count(
guest
.ssh_command_l2_1("ls /sys/bus/pci/devices")
.unwrap()
.trim(),
8
));
// Perform memory hotplug in L2 and validate the memory is showing
// up as expected. In order to check, we will use the virtio-net
// device already passed through L2 as a VFIO device, this will
// verify that VFIO devices are functional with memory hotplug.
assert!(guest.get_total_memory_l2().unwrap_or_default() > 480_000);
guest
.ssh_command_l2_1(
"sudo bash -c 'echo online > /sys/devices/system/memory/auto_online_blocks'",
)
.unwrap();
guest
.ssh_command_l1(
"sudo /mnt/ch-remote \
--api-socket=/tmp/ch_api.sock \
resize --memory=1073741824",
)
.unwrap();
assert!(guest.get_total_memory_l2().unwrap_or_default() > 960_000);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
cleanup_vfio_network_interfaces();
handle_child_output(r, &output);
}
#[test]
fn test_direct_kernel_boot_noacpi() {
let mut guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
guest.kernel_cmdline = Some(format!("{DIRECT_KERNEL_BOOT_CMDLINE} acpi=off"));
_test_direct_kernel_boot_noacpi(&guest);
}
#[test]
fn test_virtio_vsock() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_virtio_vsock(&guest, false);
}
#[test]
fn test_virtio_vsock_hotplug() {
#[cfg(target_arch = "x86_64")]
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
#[cfg(target_arch = "aarch64")]
let guest =
basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel_path(edk2_path().to_str().unwrap());
_test_virtio_vsock(&guest, true);
}
#[test]
fn test_api_http_shutdown() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4);
let target_api = TargetApi::new_http_api(&guest.tmp_dir);
_test_api_shutdown(&target_api, &guest);
}
#[test]
fn test_api_http_delete() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4);
let target_api = TargetApi::new_http_api(&guest.tmp_dir);
_test_api_delete(&target_api, &guest);
}
#[test]
fn test_api_http_pause_resume() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4);
let target_api = TargetApi::new_http_api(&guest.tmp_dir);
_test_api_pause_resume(&target_api, &guest);
}
#[test]
fn test_api_http_create_boot() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4);
let target_api = TargetApi::new_http_api(&guest.tmp_dir);
_test_api_create_boot(&target_api, &guest);
}
#[test]
fn test_virtio_iommu() {
_test_virtio_iommu(cfg!(target_arch = "x86_64"));
}
#[test]
// We cannot force the software running in the guest to reprogram the BAR
// with some different addresses, but we have a reliable way of testing it
// with a standard Linux kernel.
// By removing a device from the PCI tree, and then rescanning the tree,
// Linux consistently chooses to reorganize the PCI device BARs to other
// locations in the guest address space.
// This test creates a dedicated PCI network device to be checked as being
// properly probed first, then removing it, and adding it again by doing a
// rescan.
fn test_pci_bar_reprogramming() {
#[cfg(target_arch = "aarch64")]
let guest =
basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel_path(edk2_path().to_str().unwrap());
#[cfg(target_arch = "x86_64")]
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_pci_bar_reprogramming(&guest);
}
#[test]
fn test_memory_mergeable_off() {
test_memory_mergeable(false);
}
#[test]
#[cfg(not(feature = "mshv"))] // See issue #7435
#[cfg(target_arch = "x86_64")]
fn test_cpu_hotplug() {
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 console_str = "console=ttyS0";
let kernel_path = direct_kernel_boot_path();
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=2,max=4"])
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args([
"--cmdline",
DIRECT_KERNEL_BOOT_CMDLINE
.replace("console=hvc0", console_str)
.as_str(),
])
.args(["--serial", "tty"])
.args(["--console", "off"])
.default_disks()
.default_net()
.args(["--api-socket", &api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2);
// Resize the VM
let desired_vcpus = 4;
resize_command(&api_socket, Some(desired_vcpus), None, None, None);
guest
.ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online")
.unwrap();
guest
.ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online")
.unwrap();
assert!(wait_until(Duration::from_secs(10), || {
guest.get_cpu_count().unwrap_or_default() == u32::from(desired_vcpus)
}));
guest.reboot_linux(0);
assert_eq!(
guest.get_cpu_count().unwrap_or_default(),
u32::from(desired_vcpus)
);
// Resize the VM
let desired_vcpus = 2;
resize_command(&api_socket, Some(desired_vcpus), None, None, None);
assert!(wait_until(Duration::from_secs(10), || {
guest.get_cpu_count().unwrap_or_default() == u32::from(desired_vcpus)
}));
// Resize the VM back up to 4
let desired_vcpus = 4;
resize_command(&api_socket, Some(desired_vcpus), None, None, None);
guest
.ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online")
.unwrap();
guest
.ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online")
.unwrap();
assert!(wait_until(Duration::from_secs(10), || {
guest.get_cpu_count().unwrap_or_default() == u32::from(desired_vcpus)
}));
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_memory_hotplug() {
#[cfg(target_arch = "aarch64")]
let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string();
#[cfg(target_arch = "x86_64")]
let focal_image = FOCAL_IMAGE_NAME.to_string();
let disk_config = UbuntuDiskConfig::new(focal_image);
let guest = Guest::new(Box::new(disk_config));
let api_socket = temp_api_path(&guest.tmp_dir);
#[cfg(target_arch = "aarch64")]
let kernel_path = edk2_path();
#[cfg(target_arch = "x86_64")]
let kernel_path = direct_kernel_boot_path();
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=2,max=4"])
.args(["--memory", "size=512M,hotplug_size=8192M"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.default_net()
.args(["--balloon", "size=0"])
.args(["--api-socket", &api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
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
}));
// Use balloon to remove RAM from the VM
let desired_balloon = 512 << 20;
resize_command(&api_socket, None, None, Some(desired_balloon), None);
assert!(wait_until(Duration::from_secs(10), || {
let total_memory = guest.get_total_memory().unwrap_or_default();
total_memory > 480_000 && total_memory < 960_000
}));
guest.reboot_linux(0);
assert!(guest.get_total_memory().unwrap_or_default() < 960_000);
// Use balloon add RAM to the VM
let desired_balloon = 0;
resize_command(&api_socket, None, None, Some(desired_balloon), None);
assert!(wait_until(Duration::from_secs(10), || {
guest.get_total_memory().unwrap_or_default() > 960_000
}));
guest.enable_memory_hotplug();
// Add RAM to the VM
let desired_ram = 2048 << 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() > 1_920_000
}));
// Remove RAM to the VM (only applies after reboot)
let desired_ram = 1024 << 20;
resize_command(&api_socket, None, Some(desired_ram), None, None);
guest.reboot_linux(1);
assert!(guest.get_total_memory().unwrap_or_default() > 960_000);
assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))] // See #7456
fn test_virtio_mem() {
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 mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=2,max=4"])
.args([
"--memory",
"size=512M,hotplug_method=virtio-mem,hotplug_size=8192M",
])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.default_net()
.args(["--api-socket", &api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
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
}));
// Add RAM to the VM
let desired_ram = 2048 << 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() > 1_920_000
}));
// Remove RAM from the VM
let desired_ram = 1024 << 20;
resize_command(&api_socket, None, Some(desired_ram), None, None);
assert!(wait_until(Duration::from_secs(10), || {
let total_memory = guest.get_total_memory().unwrap_or_default();
total_memory > 960_000 && total_memory < 1_920_000
}));
guest.reboot_linux(0);
// Check the amount of memory after reboot is 1GiB
assert!(guest.get_total_memory().unwrap_or_default() > 960_000);
assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000);
// Check we can still resize to 512MiB
let desired_ram = 512 << 20;
resize_command(&api_socket, None, Some(desired_ram), None, None);
assert!(wait_until(Duration::from_secs(10), || {
let total_memory = guest.get_total_memory().unwrap_or_default();
total_memory > 480_000 && total_memory < 960_000
}));
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(target_arch = "x86_64")]
// Test both vCPU and memory resizing together
fn test_resize() {
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 mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=2,max=4"])
.args(["--memory", "size=512M,hotplug_size=8192M"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.default_net()
.args(["--api-socket", &api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2);
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
guest.enable_memory_hotplug();
// Resize the VM
let desired_vcpus = 4;
let desired_ram = 1024 << 20;
resize_command(
&api_socket,
Some(desired_vcpus),
Some(desired_ram),
None,
None,
);
guest
.ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online")
.unwrap();
guest
.ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online")
.unwrap();
assert!(wait_until(Duration::from_secs(10), || {
guest.get_cpu_count().unwrap_or_default() == u32::from(desired_vcpus)
}));
assert!(guest.get_total_memory().unwrap_or_default() > 960_000);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_memory_overhead() {
let guest_memory_size_kb: u32 = 512 * 1024;
let guest =
basic_regular_guest!(JAMMY_IMAGE_NAME).with_memory(&format!("{guest_memory_size_kb}K"));
_test_memory_overhead(&guest, guest_memory_size_kb);
}
#[test]
#[cfg(target_arch = "x86_64")]
// This test runs a guest with Landlock enabled and hotplugs a new disk. As
// the path for the hotplug disk is not pre-added to Landlock rules, this
// the test will result in a failure.
fn test_landlock() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_landlock(&guest);
}
#[test]
fn test_disk_hotplug() {
#[cfg(target_arch = "x86_64")]
let kernel_path = direct_kernel_boot_path();
#[cfg(target_arch = "aarch64")]
let kernel_path = edk2_path();
let guest =
basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel_path(kernel_path.to_str().unwrap());
_test_disk_hotplug(&guest, false);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_disk_hotplug_with_landlock() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_disk_hotplug(&guest, true);
}
#[test]
fn test_disk_resize() {
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);
// Create a disk image that we can write to
assert!(
exec_host_command_output("sudo dd if=/dev/zero of=/tmp/resize.img bs=1M count=16")
.status
.success()
);
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_disks()
.default_net()
.capture_output();
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Add the disk to the VM
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-disk",
Some("path=/tmp/resize.img,id=test0"),
);
assert!(cmd_success);
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")
);
// Check that /dev/vdc exists and the block size is 16M.
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 16M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// And check the block device can be written to.
guest
.ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=16")
.unwrap();
// Resize disk to 32M
let resize_up_success =
resize_disk_command(&api_socket, "test0", "33554432" /* 32M */);
assert!(resize_up_success);
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 32M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// And check all blocks can be written to
guest
.ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=32")
.unwrap();
// Resize down to original size
let resize_down_success =
resize_disk_command(&api_socket, "test0", "16777216" /* 16M */);
assert!(resize_down_success);
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 16M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// And check all blocks can be written to, again
guest
.ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=16")
.unwrap();
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_disk_resize_qcow2() {
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);
let test_disk_path = guest.tmp_dir.as_path().join("resize-test.qcow2");
// Create a 16MB QCOW2 disk image
assert!(
exec_host_command_output(&format!(
"qemu-img create -f qcow2 {} 16M",
test_disk_path.to_str().unwrap()
))
.status
.success()
);
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_disks()
.default_net()
.capture_output();
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Add the QCOW2 disk to the VM
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-disk",
Some(&format!(
"path={},id=test0",
test_disk_path.to_str().unwrap()
)),
);
assert!(cmd_success);
assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"test0\""));
// Check that /dev/vdc exists and the block size is 16M
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 16M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Write some data to verify it persists after resize
guest
.ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=8")
.unwrap();
// Resize disk up to 32M
let resize_up_success =
resize_disk_command(&api_socket, "test0", "33554432" /* 32M */);
assert!(resize_up_success);
// Check new size is visible
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 32M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Write to the expanded area to verify it works
guest
.ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=32")
.unwrap();
// Resize to 64M to exercise L1 table growth
let resize_up_again_success =
resize_disk_command(&api_socket, "test0", "67108864" /* 64M */);
assert!(resize_up_again_success);
assert_eq!(
guest
.ssh_command("lsblk | grep vdc | grep -c 64M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Write to the full disk
guest
.ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=64")
.unwrap();
// QCOW2 does not support shrinking, no resize down test here.
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
disk_check_consistency(&test_disk_path, None);
handle_child_output(r, &output);
}
fn create_loop_device(backing_file_path: &str, block_size: u32, num_retries: usize) -> String {
const LOOP_CONFIGURE: u64 = 0x4c0a;
const LOOP_CTL_GET_FREE: u64 = 0x4c82;
const LOOP_CTL_PATH: &str = "/dev/loop-control";
const LOOP_DEVICE_PREFIX: &str = "/dev/loop";
#[repr(C)]
struct LoopInfo64 {
lo_device: u64,
lo_inode: u64,
lo_rdevice: u64,
lo_offset: u64,
lo_sizelimit: u64,
lo_number: u32,
lo_encrypt_type: u32,
lo_encrypt_key_size: u32,
lo_flags: u32,
lo_file_name: [u8; 64],
lo_crypt_name: [u8; 64],
lo_encrypt_key: [u8; 32],
lo_init: [u64; 2],
}
impl Default for LoopInfo64 {
fn default() -> Self {
LoopInfo64 {
lo_device: 0,
lo_inode: 0,
lo_rdevice: 0,
lo_offset: 0,
lo_sizelimit: 0,
lo_number: 0,
lo_encrypt_type: 0,
lo_encrypt_key_size: 0,
lo_flags: 0,
lo_file_name: [0; 64],
lo_crypt_name: [0; 64],
lo_encrypt_key: [0; 32],
lo_init: [0; 2],
}
}
}
#[derive(Default)]
#[repr(C)]
struct LoopConfig {
fd: u32,
block_size: u32,
info: LoopInfo64,
_reserved: [u64; 8],
}
// Open loop-control device
let loop_ctl_file = OpenOptions::new()
.read(true)
.write(true)
.open(LOOP_CTL_PATH)
.unwrap();
// Open backing file
let backing_file = OpenOptions::new()
.read(true)
.write(true)
.open(backing_file_path)
.unwrap();
// Retry the whole get free -> open -> configure sequence so that a
// race with another parallel test claiming the same loop device
// is resolved by requesting a new free device on each attempt.
let mut loop_device_path = String::new();
for i in 0..num_retries {
// Request a free loop device
let loop_device_number =
unsafe { libc::ioctl(loop_ctl_file.as_raw_fd(), LOOP_CTL_GET_FREE as _) };
if loop_device_number < 0 {
panic!("Couldn't find a free loop device");
}
loop_device_path = format!("{LOOP_DEVICE_PREFIX}{loop_device_number}");
// Open loop device
let loop_device_file = OpenOptions::new()
.read(true)
.write(true)
.open(&loop_device_path)
.unwrap();
let loop_config = LoopConfig {
fd: backing_file.as_raw_fd() as u32,
block_size,
..Default::default()
};
let ret = unsafe {
libc::ioctl(
loop_device_file.as_raw_fd(),
LOOP_CONFIGURE as _,
&loop_config,
)
};
if ret == 0 {
break;
}
if i < num_retries - 1 {
println!(
"Iteration {}: Failed to configure loop device {}: {}",
i,
loop_device_path,
io::Error::last_os_error()
);
let jitter_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.subsec_nanos()
% 500
+ 100;
thread::sleep(Duration::from_millis(jitter_ms as u64));
} else {
panic!(
"Failed {} times trying to configure the loop device {}: {}",
num_retries,
loop_device_path,
io::Error::last_os_error()
);
}
}
loop_device_path
}
#[test]
fn test_virtio_block_topology() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
let test_disk_path = guest.tmp_dir.as_path().join("test.img");
let output = exec_host_command_output(
format!(
"qemu-img create -f raw {} 16M",
test_disk_path.to_str().unwrap()
)
.as_str(),
);
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
panic!("qemu-img command failed\nstdout\n{stdout}\nstderr\n{stderr}");
}
let loop_dev = create_loop_device(test_disk_path.to_str().unwrap(), 4096, 5);
_test_virtio_block_topology(&guest, &loop_dev);
Command::new("losetup")
.args(["-d", &loop_dev])
.output()
.expect("loop device not found");
}
#[test]
fn test_virtio_block_direct_io_block_device_alignment_4k() {
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();
// The backing file for the loop device must live on a filesystem that
// supports O_DIRECT (e.g. ext4). guest.tmp_dir is on tmpfs inside
// Docker, and the loop driver forwards I/O to the backing file.
let mut workloads_path = dirs::home_dir().unwrap();
workloads_path.push("workloads");
let img_dir = TempDir::new_in(workloads_path.as_path()).unwrap();
let test_disk_path = img_dir.as_path().join("directio_test.img");
// Preallocate the backing file -- a sparse file can deadlock when
// O_DIRECT writes through a loop device trigger block allocation
// in the backing filesystem.
assert!(
exec_host_command_output(&format!(
"fallocate -l 64M {}",
test_disk_path.to_str().unwrap()
))
.status
.success(),
"fallocate failed"
);
let loop_dev = create_loop_device(test_disk_path.to_str().unwrap(), 4096, 5);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=1"])
.args(["--memory", "size=512M"])
.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(),
format!("path={},direct=on,image_type=raw", &loop_dev).as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("lsblk -t | grep vdc | awk '{print $6}'")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
4096
);
guest
.ssh_command(
"sudo dd if=/dev/urandom of=/tmp/pattern bs=4096 count=1 && \
sudo dd if=/tmp/pattern of=/dev/vdc bs=4096 count=1 seek=1 oflag=direct && \
sudo dd if=/dev/vdc of=/tmp/readback bs=4096 count=1 skip=1 iflag=direct && \
cmp /tmp/pattern /tmp/readback",
)
.unwrap();
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
Command::new("losetup")
.args(["-d", &loop_dev])
.output()
.expect("loop device cleanup failed");
}
#[test]
fn test_virtio_block_direct_io_file_backed_alignment_4k() {
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 mut workloads_path = dirs::home_dir().unwrap();
workloads_path.push("workloads");
let img_dir = TempDir::new_in(workloads_path.as_path()).unwrap();
let fs_img_path = img_dir.as_path().join("fs_4ksec.img");
assert!(
exec_host_command_output(&format!(
"truncate -s 512M {}",
fs_img_path.to_str().unwrap()
))
.status
.success(),
"truncate failed"
);
let loop_dev_path = create_loop_device(fs_img_path.to_str().unwrap(), 4096, 5);
assert!(
exec_host_command_output(&format!("mkfs.ext4 -q {loop_dev_path}"))
.status
.success(),
"mkfs.ext4 failed"
);
let mnt_dir = img_dir.as_path().join("mnt");
fs::create_dir_all(&mnt_dir).unwrap();
assert!(
exec_host_command_output(&format!(
"mount {} {}",
&loop_dev_path,
mnt_dir.to_str().unwrap()
))
.status
.success(),
"mount failed"
);
let test_disk_path = mnt_dir.join("dio_file_test.raw");
assert!(
exec_host_command_output(&format!(
"truncate -s 64M {}",
test_disk_path.to_str().unwrap()
))
.status
.success(),
"truncate test disk failed"
);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=1"])
.args(["--memory", "size=512M"])
.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(),
format!(
"path={},direct=on,image_type=raw",
test_disk_path.to_str().unwrap()
)
.as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
let log_sec: u32 = guest
.ssh_command("lsblk -t | grep vdc | awk '{print $6}'")
.unwrap()
.trim()
.parse()
.unwrap_or_default();
assert_eq!(
log_sec, 4096,
"expected 4096-byte logical sector for file on 4k-sector fs, got {log_sec}"
);
guest
.ssh_command(
"sudo dd if=/dev/urandom of=/tmp/pattern bs=4096 count=8 && \
sudo dd if=/tmp/pattern of=/dev/vdc bs=4096 count=8 seek=1 oflag=direct && \
sudo dd if=/dev/vdc of=/tmp/readback bs=4096 count=8 skip=1 iflag=direct && \
cmp /tmp/pattern /tmp/readback",
)
.unwrap();
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
let _ = exec_host_command_output(&format!("umount {}", mnt_dir.to_str().unwrap()));
let _ = exec_host_command_output(&format!("losetup -d {loop_dev_path}"));
}
// Helper function to verify sparse file
fn verify_sparse_file(test_disk_path: &str, expected_ratio: f64) {
let res = exec_host_command_output(&format!("ls -s --block-size=1 {test_disk_path}"));
assert!(res.status.success(), "ls -s command failed");
let out = String::from_utf8_lossy(&res.stdout);
let actual_bytes: u64 = out
.split_whitespace()
.next()
.and_then(|s| s.parse().ok())
.expect("Failed to parse ls -s output");
let res = exec_host_command_output(&format!("ls -l {test_disk_path}"));
assert!(res.status.success(), "ls -l command failed");
let out = String::from_utf8_lossy(&res.stdout);
let apparent_size: u64 = out
.split_whitespace()
.nth(4)
.and_then(|s| s.parse().ok())
.expect("Failed to parse ls -l output");
let threshold = (apparent_size as f64 * expected_ratio) as u64;
assert!(
actual_bytes < threshold,
"Expected file to be sparse: apparent_size={apparent_size} bytes, actual_disk_usage={actual_bytes} bytes (threshold={threshold})"
);
}
// Helper function to count zero flagged regions in QCOW2 image
fn count_qcow2_zero_regions(test_disk_path: &str) -> Option<usize> {
let res =
exec_host_command_output(&format!("qemu-img map --output=json -U {test_disk_path}"));
if !res.status.success() {
return None;
}
let out = String::from_utf8_lossy(&res.stdout);
let map_json = serde_json::from_str::<serde_json::Value>(&out).ok()?;
let regions = map_json.as_array()?;
Some(
regions
.iter()
.filter(|r| {
let data = r["data"].as_bool().unwrap_or(true);
let zero = r["zero"].as_bool().unwrap_or(false);
// holes - data: false
// zero flagged regions - data: true, zero: true
!data || zero
})
.count(),
)
}
// Helper function to verify file extents using FIEMAP after DISCARD
// TODO: Make verification more format-specific:
// - QCOW2: Check for fragmentation patterns showing deallocated clusters
// - RAW: Verify actual holes (unallocated extents) exist in sparse regions
// - Could parse extent output to count holes vs allocated regions
fn verify_fiemap_extents(test_disk_path: &str, format_type: &str) {
let blocksize_output = exec_host_command_output(&format!("stat -f -c %S {test_disk_path}"));
let blocksize = if blocksize_output.status.success() {
String::from_utf8_lossy(&blocksize_output.stdout)
.trim()
.parse::<u64>()
.unwrap_or(4096)
} else {
4096
};
let fiemap_output =
exec_host_command_output(&format!("filefrag -b {blocksize} -v {test_disk_path}"));
if fiemap_output.status.success() {
let fiemap_str = String::from_utf8_lossy(&fiemap_output.stdout);
// Verify we have extent information indicating sparse regions
let has_extents = fiemap_str.contains("extent") || fiemap_str.contains("extents");
let has_holes = fiemap_str.contains("hole");
assert!(
has_extents || has_holes,
"FIEMAP should show extent information or holes for {format_type} file"
);
}
}
/// Helper function to verify a disk region reads as all zeros from within the guest
fn assert_guest_disk_region_is_zero(guest: &Guest, device: &str, offset: u64, length: u64) {
let result = guest
.ssh_command(&format!(
"sudo hexdump -v -s {offset} -n {length} -e '1/1 \"%02x\"' {device} | grep -qv '^00*$' && echo 'NONZERO' || echo 'ZEROS'"
))
.unwrap();
assert!(
result.trim() == "ZEROS",
"Expected {} region at offset {} length {} to read as zeros, but got: {}",
device,
offset,
length,
result.trim()
);
}
// Common test sizes for discard/fstrim tests (all formats): 9 small (≤256KB), then one 4MB
const BLOCK_DISCARD_TEST_SIZES_KB: &[u64] = &[64, 128, 256, 64, 128, 256, 64, 128, 256, 4096];
fn _test_virtio_block_discard(
format_name: &str,
qemu_img_format: &str,
extra_create_args: &[&str],
expect_discard_success: bool,
verify_disk: bool,
) {
_test_virtio_block_discard_with_backend(
format_name,
qemu_img_format,
extra_create_args,
expect_discard_success,
verify_disk,
false,
);
}
fn _test_virtio_block_discard_with_backend(
format_name: &str,
qemu_img_format: &str,
extra_create_args: &[&str],
expect_discard_success: bool,
verify_disk: bool,
disable_io_uring: 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 test_disk_path = guest
.tmp_dir
.as_path()
.join(format!("discard_test.{}", format_name.to_lowercase()));
let mut cmd = format!("qemu-img create -f {qemu_img_format} ");
if !extra_create_args.is_empty() {
cmd.push_str(&extra_create_args.join(" "));
cmd.push(' ');
}
cmd.push_str(&format!("{} 2G", test_disk_path.to_str().unwrap()));
let res = exec_host_command_output(&cmd);
assert!(
res.status.success(),
"Failed to create {format_name} test image"
);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=4"])
.default_memory()
.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(),
format!(
"path={},num_queues=4,image_type={}{}",
test_disk_path.to_str().unwrap(),
format_name.to_lowercase(),
if disable_io_uring {
",_disable_io_uring=on"
} else {
""
}
)
.as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
const CLUSTER_SIZE_BYTES: u64 = 64 * 1024; // One QCOW2 cluster
const WRITE_SIZE_MB: u64 = 4;
const WRITE_OFFSET_MB: u64 = 1;
// Build discard operations within the written region
let write_start = WRITE_OFFSET_MB * 1024 * 1024;
let mut discard_operations: Vec<(u64, u64)> = Vec::new();
let mut current_offset = write_start;
for &size_kb in BLOCK_DISCARD_TEST_SIZES_KB {
let size = size_kb * 1024;
discard_operations.push((current_offset, size));
current_offset += size + CLUSTER_SIZE_BYTES; // Add gap between operations
}
let size_after_write = std::cell::Cell::new(0u64);
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("lsblk | grep -c vdc")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// Write one 4MB block at offset 1MB
guest
.ssh_command(&format!(
"sudo dd if=/dev/zero of=/dev/vdc bs=1M count={WRITE_SIZE_MB} seek={WRITE_OFFSET_MB} oflag=direct"
))
.unwrap();
guest.ssh_command("sync").unwrap();
// For QCOW2, measure file size after write to verify deallocation later
let write_size = if qemu_img_format == "qcow2" {
let res = exec_host_command_output(&format!(
"ls -s --block-size=1 {}",
test_disk_path.to_str().unwrap()
));
assert!(res.status.success());
String::from_utf8_lossy(&res.stdout)
.split_whitespace()
.next()
.and_then(|s| s.parse::<u64>().ok())
.expect("Failed to parse file size after write")
} else {
0
};
size_after_write.set(write_size);
if expect_discard_success {
for (i, (offset, length)) in discard_operations.iter().enumerate() {
let result = guest
.ssh_command(&format!(
"sudo blkdiscard -v -o {offset} -l {length} /dev/vdc 2>&1 || true"
))
.unwrap();
assert!(
!result.contains("Operation not supported")
&& !result.contains("BLKDISCARD"),
"blkdiscard #{i} at offset {offset} length {length} failed: {result}"
);
}
// Force sync to ensure async DISCARD operations complete
guest.ssh_command("sync").unwrap();
// Verify VM sees zeros in discarded regions
for (offset, length) in discard_operations.iter() {
assert_guest_disk_region_is_zero(&guest, "/dev/vdc", *offset, *length);
}
guest.ssh_command("echo test").unwrap();
} else {
// For unsupported formats, blkdiscard should fail with "not supported"
use test_infra::ssh_command_ip;
let result = ssh_command_ip(
"sudo blkdiscard -o 0 -l 4096 /dev/vdc 2>&1",
&guest.network.guest_ip0,
0,
5,
);
assert!(
result.is_err(),
"blkdiscard should fail on unsupported format"
);
guest.ssh_command("echo test").unwrap();
}
if expect_discard_success {
if qemu_img_format == "qcow2" {
let res = exec_host_command_output(&format!(
"ls -s --block-size=1 {}",
test_disk_path.to_str().unwrap()
));
assert!(res.status.success());
let size_after_discard: u64 = String::from_utf8_lossy(&res.stdout)
.split_whitespace()
.next()
.and_then(|s| s.parse().ok())
.expect("Failed to parse file size after discard");
assert!(
size_after_discard < size_after_write.get(),
"QCOW2 file should shrink after DISCARD with sparse=true: after_write={} bytes, after_discard={} bytes",
size_after_write.get(),
size_after_discard
);
verify_fiemap_extents(test_disk_path.to_str().unwrap(), "QCOW2");
} else if qemu_img_format == "raw" {
let mut file = File::open(&test_disk_path)
.expect("Failed to open test disk for verification");
// Verify each discarded region contains all zeros
for (offset, length) in &discard_operations {
file.seek(SeekFrom::Start(*offset))
.expect("Failed to seek to discarded region");
let mut buffer = vec![0u8; *length as usize];
file.read_exact(&mut buffer)
.expect("Failed to read discarded region");
let all_zeros = buffer.iter().all(|&b| b == 0);
assert!(
all_zeros,
"Expected discarded region at offset {offset} length {length} to contain all zeros"
);
}
verify_sparse_file(test_disk_path.to_str().unwrap(), 1.0);
verify_fiemap_extents(test_disk_path.to_str().unwrap(), "RAW");
}
}
}));
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
if verify_disk {
disk_check_consistency(&test_disk_path, None);
}
}
#[test]
fn test_virtio_block_discard_qcow2() {
_test_virtio_block_discard("qcow2", "qcow2", &[], true, true);
}
#[test]
fn test_virtio_block_discard_raw() {
_test_virtio_block_discard("raw", "raw", &[], true, false);
}
#[test]
fn test_virtio_block_discard_raw_aio() {
_test_virtio_block_discard_with_backend("raw", "raw", &[], true, false, true);
}
#[test]
fn test_virtio_block_write_zeroes_unmap_raw() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let test_disk_path = guest.tmp_dir.as_path().join("write_zeroes_unmap_test.raw");
let res = exec_host_command_output(&format!(
"dd if=/dev/zero of={} bs=1M count=128",
test_disk_path.to_str().unwrap()
));
assert!(res.status.success(), "Failed to create raw test image");
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={},image_type=raw", test_disk_path.to_str().unwrap()).as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("lsblk | grep -c vdc")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
let wz_max = guest
.ssh_command("cat /sys/block/vdc/queue/write_zeroes_max_bytes")
.unwrap()
.trim()
.parse::<u64>()
.unwrap_or_default();
assert!(
wz_max > 0,
"write_zeroes_max_bytes={wz_max}, VIRTIO_BLK_F_WRITE_ZEROES not negotiated"
);
guest
.ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=64 oflag=direct")
.unwrap();
guest.ssh_command("sync").unwrap();
// fallocate --punch-hole on a block device sends
// WRITE_ZEROES with VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP set.
let result = guest
.ssh_command("sudo fallocate -p -o 0 -l 67108864 /dev/vdc 2>&1 || true")
.unwrap();
assert!(
!result.contains("Operation not supported") && !result.contains("not supported"),
"fallocate --punch-hole failed: {result}"
);
guest.ssh_command("sync").unwrap();
assert_guest_disk_region_is_zero(&guest, "/dev/vdc", 0, 4096 * 256);
let test_disk_str = test_disk_path.to_str().unwrap();
verify_sparse_file(test_disk_str, 1.0);
verify_fiemap_extents(test_disk_str, "raw");
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_virtio_block_discard_unsupported_vhd() {
_test_virtio_block_discard("vhd", "vpc", &["-o", "subformat=fixed"], false, false);
}
#[test]
fn test_virtio_block_discard_unsupported_vhdx() {
_test_virtio_block_discard("vhdx", "vhdx", &[], false, false);
}
#[test]
fn test_virtio_block_discard_loop_device() {
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 test_disk_path = guest.tmp_dir.as_path().join("loop_discard_test.raw");
let res = run_qemu_img(&test_disk_path, &["create", "-f", "raw"], Some(&["128M"]));
assert!(
res.status.success(),
"Failed to create raw backing image: {}",
String::from_utf8_lossy(&res.stderr)
);
let loop_dev = create_loop_device(test_disk_path.to_str().unwrap(), 4096, 5);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=1"])
.args(["--memory", "size=512M"])
.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(),
format!("path={},image_type=raw", &loop_dev).as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("lsblk | grep -c vdc")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
assert_eq!(
guest
.ssh_command("lsblk -t | grep vdc | awk '{print $6}'")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
4096
);
let discard_max = guest
.ssh_command("cat /sys/block/vdc/queue/discard_max_bytes")
.unwrap()
.trim()
.parse::<u64>()
.unwrap_or_default();
assert!(
discard_max > 0,
"discard_max_bytes={discard_max}, VIRTIO_BLK_F_DISCARD not negotiated"
);
guest
.ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=4096 count=1024 oflag=direct")
.unwrap();
guest.ssh_command("sync").unwrap();
let result = guest
.ssh_command("sudo blkdiscard -v -o 0 -l 4194304 /dev/vdc 2>&1 || true")
.unwrap();
assert!(
!result.contains("Operation not supported")
&& !result.contains("BLKDISCARD ioctl failed"),
"blkdiscard failed on loop device: {result}"
);
guest.ssh_command("sync").unwrap();
assert_guest_disk_region_is_zero(&guest, "/dev/vdc", 0, 4194304);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
Command::new("losetup")
.args(["-d", &loop_dev])
.output()
.expect("loop device not found");
}
#[test]
fn test_virtio_block_discard_dm_snapshot() {
// Verify that the guest remains stable when BLKDISCARD fails on the
// host backend. DM snapshot targets do not support discard, so the
// VMM returns VIRTIO_BLK_S_IOERR. The guest must handle this
// gracefully even under repeated attempts.
//
// DM topology follows the same pattern used by WindowsDiskConfig.
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 origin_path = guest.tmp_dir.as_path().join("dm_origin.raw");
let cow_path = guest.tmp_dir.as_path().join("dm_cow.raw");
let res = run_qemu_img(&origin_path, &["create", "-f", "raw"], Some(&["128M"]));
assert!(
res.status.success(),
"Failed to create origin image: {}",
String::from_utf8_lossy(&res.stderr)
);
let cow_size: u64 = 128 << 20;
let cow_sectors = cow_size / 512;
let cow_file = File::create(&cow_path).expect("Expect creating COW image to succeed");
cow_file
.set_len(cow_size)
.expect("Expect truncating COW image to succeed");
let origin_sectors: u64 = 128 * 1024 * 1024 / 512;
let origin_loop = create_loop_device(origin_path.to_str().unwrap(), 4096, 5);
let cow_loop = create_loop_device(cow_path.to_str().unwrap(), 512, 5);
let unique = format!(
"ch-test-{}",
guest
.tmp_dir
.as_path()
.file_name()
.unwrap()
.to_str()
.unwrap()
);
let cow_dm_name = format!("{unique}-cow");
let snap_dm_name = format!("{unique}-snap");
let output = Command::new("dmsetup")
.args([
"create",
&cow_dm_name,
"--table",
&format!("0 {cow_sectors} linear {cow_loop} 0"),
])
.output()
.expect("Failed to run dmsetup");
assert!(
output.status.success(),
"dmsetup create (cow linear) failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Command::new("dmsetup")
.arg("mknodes")
.output()
.expect("dmsetup mknodes failed");
// dm-snapshot: origin + COW, non-persistent, chunk size 8 sectors.
let output = Command::new("dmsetup")
.args([
"create",
&snap_dm_name,
"--table",
&format!("0 {origin_sectors} snapshot {origin_loop} /dev/mapper/{cow_dm_name} N 8"),
])
.output()
.expect("Failed to run dmsetup");
assert!(
output.status.success(),
"dmsetup create (snapshot) failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Command::new("dmsetup")
.arg("mknodes")
.output()
.expect("dmsetup mknodes failed");
let dm_dev = format!("/dev/mapper/{snap_dm_name}");
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=1"])
.args(["--memory", "size=512M"])
.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(),
format!("path={},image_type=raw", &dm_dev).as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("lsblk | grep -c vdc")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
let discard_max = guest
.ssh_command("cat /sys/block/vdc/queue/discard_max_bytes")
.unwrap()
.trim()
.parse::<u64>()
.unwrap_or_default();
assert!(
discard_max > 0,
"discard_max_bytes={discard_max}, VIRTIO_BLK_F_DISCARD not negotiated"
);
guest
.ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=4096 count=1024 oflag=direct")
.unwrap();
guest.ssh_command("sync").unwrap();
// Discard is expected to fail on DM snapshot because the
// snapshot target does not support BLKDISCARD.
for attempt in 1..=3 {
let result = guest
.ssh_command("sudo blkdiscard -o 0 -l 4194304 /dev/vdc 2>&1; echo rc=$?")
.unwrap();
println!("blkdiscard attempt {attempt}: {result}");
let uptime = guest.ssh_command("uptime").unwrap();
assert!(
!uptime.is_empty(),
"Guest unresponsive after blkdiscard attempt {attempt}"
);
}
guest
.ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=4096 count=256 oflag=direct")
.unwrap();
let readback = guest
.ssh_command("sudo dd if=/dev/vdc bs=4096 count=1 iflag=direct 2>/dev/null | od -A n -t x1 | head -1")
.unwrap();
assert!(
!readback.trim().is_empty(),
"Failed to read back from device after discard errors"
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
let _ = Command::new("dmsetup")
.args(["remove", &snap_dm_name])
.output();
let _ = Command::new("dmsetup")
.args(["remove", &cow_dm_name])
.output();
let _ = Command::new("losetup").args(["-d", &origin_loop]).output();
let _ = Command::new("losetup").args(["-d", &cow_loop]).output();
}
fn _test_virtio_block_fstrim(
format_name: &str,
qemu_img_format: &str,
extra_create_args: &[&str],
expect_fstrim_success: bool,
verify_disk: bool,
) {
_test_virtio_block_fstrim_with_backend(
format_name,
qemu_img_format,
extra_create_args,
expect_fstrim_success,
verify_disk,
false,
);
}
fn _test_virtio_block_fstrim_with_backend(
format_name: &str,
qemu_img_format: &str,
extra_create_args: &[&str],
expect_fstrim_success: bool,
verify_disk: bool,
disable_io_uring: 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 test_disk_path = guest
.tmp_dir
.as_path()
.join(format!("fstrim_test.{}", format_name.to_lowercase()));
let mut cmd = format!("qemu-img create -f {qemu_img_format} ");
if !extra_create_args.is_empty() {
cmd.push_str(&extra_create_args.join(" "));
cmd.push(' ');
}
cmd.push_str(&format!("{} 2G", test_disk_path.to_str().unwrap()));
let res = exec_host_command_output(&cmd);
assert!(
res.status.success(),
"Failed to create {format_name} test image"
);
const WRITE_SIZE_MB: u64 = 4;
const CLUSTER_SIZE_BYTES: u64 = 64 * 1024;
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=4"])
.default_memory()
.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(),
format!(
"path={},num_queues=4,image_type={}{}",
test_disk_path.to_str().unwrap(),
format_name.to_lowercase(),
if disable_io_uring {
",_disable_io_uring=on"
} else {
""
}
)
.as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let max_size_during_writes = std::cell::Cell::new(0u64);
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("lsblk | grep -c vdc")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
guest.ssh_command("sudo mkfs.ext4 -F /dev/vdc").unwrap();
guest
.ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test")
.unwrap();
for (iteration, &write_size_kb) in BLOCK_DISCARD_TEST_SIZES_KB.iter().enumerate() {
guest
.ssh_command(&format!(
"sudo dd if=/dev/zero of=/mnt/test/testfile{iteration} bs=1K count={write_size_kb}"
))
.unwrap();
guest.ssh_command("sync").unwrap();
// Measure QCOW2 file size after writing
if qemu_img_format == "qcow2" {
let res = exec_host_command_output(&format!(
"ls -s --block-size=1 {}",
test_disk_path.to_str().unwrap()
));
if res.status.success()
&& let Some(size) = String::from_utf8_lossy(&res.stdout)
.split_whitespace()
.next()
.and_then(|s| s.parse::<u64>().ok())
{
max_size_during_writes.set(max_size_during_writes.get().max(size));
}
}
// Make blocks available for discard
guest
.ssh_command(&format!("sudo rm /mnt/test/testfile{iteration}"))
.unwrap();
guest.ssh_command("sync").unwrap();
if expect_fstrim_success {
let fstrim_result = guest.ssh_command("sudo fstrim -v /mnt/test 2>&1").unwrap();
// Would output like "/mnt/test: X bytes (Y MB) trimmed"
assert!(
fstrim_result.contains("trimmed") || fstrim_result.contains("bytes"),
"fstrim iteration {iteration} ({write_size_kb}KB) should report trimmed bytes: {fstrim_result}"
);
} else {
// For unsupported formats, expect fstrim to fail
use test_infra::ssh_command_ip;
let result = ssh_command_ip(
"sudo fstrim -v /mnt/test 2>&1",
&guest.network.guest_ip0,
0,
5,
);
assert!(result.is_err(), "fstrim should fail on unsupported format");
guest.ssh_command("echo 'VM responsive'").unwrap();
}
}
guest.ssh_command("sudo umount /mnt/test").unwrap();
guest.ssh_command("echo test").unwrap();
}));
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
if expect_fstrim_success {
if qemu_img_format == "qcow2" {
// Verify QCOW2 file shrank after fstrim (sparse=true deallocates clusters)
let res = exec_host_command_output(&format!(
"ls -s --block-size=1 {}",
test_disk_path.to_str().unwrap()
));
assert!(res.status.success());
let size_after_fstrim: u64 = String::from_utf8_lossy(&res.stdout)
.split_whitespace()
.next()
.and_then(|s| s.parse().ok())
.expect("Failed to parse file size after fstrim");
assert!(
size_after_fstrim < max_size_during_writes.get(),
"QCOW2 file should shrink after fstrim with sparse=true: max_during_writes={} bytes, after_fstrim={} bytes",
max_size_during_writes.get(),
size_after_fstrim
);
} else if qemu_img_format == "raw" {
verify_sparse_file(test_disk_path.to_str().unwrap(), 0.5);
}
}
handle_child_output(r, &output);
if verify_disk {
disk_check_consistency(&test_disk_path, None);
}
}
#[test]
fn test_virtio_block_fstrim_qcow2() {
_test_virtio_block_fstrim("qcow2", "qcow2", &[], true, true);
}
#[test]
fn test_virtio_block_fstrim_raw() {
_test_virtio_block_fstrim("raw", "raw", &[], true, false);
}
#[test]
fn test_virtio_block_fstrim_raw_aio() {
_test_virtio_block_fstrim_with_backend("raw", "raw", &[], true, false, true);
}
#[test]
fn test_virtio_block_fstrim_unsupported_vhd() {
_test_virtio_block_fstrim("vhd", "vpc", &["-o", "subformat=fixed"], false, false);
}
#[test]
fn test_virtio_block_fstrim_unsupported_vhdx() {
_test_virtio_block_fstrim("vhdx", "vhdx", &[], false, false);
}
#[test]
#[ignore = "fallocate() preallocation requires native filesystem support (fails on overlay/tmpfs in CI)"]
fn test_virtio_block_sparse_off_raw() {
const TEST_DISK_SIZE: &str = "2G";
const TEST_DISK_SIZE_BYTES: u64 = 2 * 1024 * 1024 * 1024;
const INITIAL_ALLOCATION_THRESHOLD: u64 = 1024 * 1024;
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 test_disk_path = guest.tmp_dir.as_path().join("sparse_off_test.raw");
let test_disk_path = test_disk_path.to_str().unwrap();
let res =
exec_host_command_output(&format!("truncate -s {TEST_DISK_SIZE} {test_disk_path}"));
assert!(res.status.success(), "Failed to create sparse test file");
let res = exec_host_command_output(&format!("ls -s --block-size=1 {test_disk_path}"));
assert!(res.status.success());
let initial_bytes: u64 = String::from_utf8_lossy(&res.stdout)
.split_whitespace()
.next()
.and_then(|s| s.parse().ok())
.expect("Failed to parse initial disk usage");
assert!(
initial_bytes < INITIAL_ALLOCATION_THRESHOLD,
"File should be initially sparse: {initial_bytes} bytes allocated"
);
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={}",
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},sparse=off").as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("lsblk | grep -c vdc")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
// After VM starts with sparse=off, verify file is fully allocated.
// Strategy is to compare compare physical vs logical bytes
// - physical >= logical is fully allocated, modulo block alignment
// - physical < logical is still sparse
let res = exec_host_command_output(&format!("ls -l {test_disk_path}"));
assert!(res.status.success());
let logical_size: u64 = String::from_utf8_lossy(&res.stdout)
.split_whitespace()
.nth(4)
.and_then(|s| s.parse().ok())
.expect("Failed to parse logical size");
let res = exec_host_command_output(&format!("ls -s --block-size=1 {test_disk_path}"));
assert!(res.status.success());
let physical_size: u64 = String::from_utf8_lossy(&res.stdout)
.split_whitespace()
.next()
.and_then(|s| s.parse().ok())
.expect("Failed to parse physical size");
assert_eq!(
logical_size, TEST_DISK_SIZE_BYTES,
"Logical size should be exactly {TEST_DISK_SIZE_BYTES} bytes, got {logical_size}"
);
let res = exec_host_command_output(&format!("stat -c '%o' {test_disk_path}"));
assert!(res.status.success());
let block_size: u64 = String::from_utf8_lossy(&res.stdout)
.trim()
.parse()
.expect("Failed to parse block size from stat");
let expected_max = logical_size.div_ceil(block_size) * block_size;
assert!(
physical_size >= logical_size,
"File should be fully allocated with sparse=off: logical={logical_size} bytes, physical={physical_size} bytes (physical < logical means still sparse)"
);
assert!(
physical_size <= expected_max,
"Physical size seems too large: logical={logical_size} bytes, physical={physical_size} bytes, expected_max={expected_max} bytes (block_size={block_size})"
);
}
#[test]
fn test_virtio_block_sparse_off_qcow2() {
const TEST_DISK_SIZE: &str = "2G";
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 test_disk_path = guest.tmp_dir.as_path().join("sparse_off_test.qcow2");
let test_disk_path = test_disk_path.to_str().unwrap();
let res = exec_host_command_output(&format!(
"qemu-img create -f qcow2 {test_disk_path} {TEST_DISK_SIZE}"
));
assert!(res.status.success(), "Failed to create QCOW2 test image");
let zero_regions_before = count_qcow2_zero_regions(test_disk_path)
.expect("Failed to get initial zero regions count");
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=4"])
.default_memory()
.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(),
format!("path={test_disk_path},sparse=off,num_queues=4").as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest
.ssh_command("lsblk | grep -c vdc")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
// With sparse=off, DISCARD should NOT be advertised.
// blkdiscard is expected to fail.
let discard_result =
guest.ssh_command("sudo blkdiscard -o 1048576 -l 1048576 /dev/vdc 2>&1; echo $?");
let exit_code = discard_result
.unwrap()
.trim()
.lines()
.last()
.unwrap_or("1")
.parse::<u32>()
.unwrap_or(1);
assert_ne!(
exit_code, 0,
"blkdiscard should fail with sparse=off (DISCARD not advertised)"
);
// WRITE_ZEROES should still work via blkdiscard --zeroout
guest
.ssh_command(
"sudo dd if=/dev/urandom of=/dev/vdc bs=1K count=64 seek=1024 oflag=direct",
)
.unwrap();
guest.ssh_command("sync").unwrap();
guest
.ssh_command("sudo blkdiscard -z -o 1048576 -l 65536 /dev/vdc")
.unwrap();
guest.ssh_command("sync").unwrap();
assert_guest_disk_region_is_zero(&guest, "/dev/vdc", 1048576, 65536);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
let zero_regions_after = count_qcow2_zero_regions(test_disk_path)
.expect("Failed to get final zero regions count");
handle_child_output(r, &output);
// WRITE_ZEROES should still produce zero-flagged regions
assert!(
zero_regions_after > zero_regions_before,
"Expected zero-flagged regions to increase via WRITE_ZEROES: before={zero_regions_before}, after={zero_regions_after}"
);
disk_check_consistency(test_disk_path, None);
}
#[test]
fn test_virtio_balloon_deflate_on_oom() {
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 api_socket = temp_api_path(&guest.tmp_dir);
//Let's start a 4G guest with balloon occupied 2G memory
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket])
.default_cpus()
.args(["--memory", "size=4G"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args(["--balloon", "size=2G,deflate_on_oom=on"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Wait for balloon memory's initialization and check its size.
// The virtio-balloon driver might take a few seconds to report the
// balloon effective size back to the VMM.
assert!(wait_until(Duration::from_secs(20), || {
balloon_size(&api_socket) == 2147483648
}));
let orig_balloon = balloon_size(&api_socket);
println!("The original balloon memory size is {orig_balloon} bytes");
assert!(orig_balloon == 2147483648);
// Two steps to verify if the 'deflate_on_oom' parameter works.
// 1st: run a command to trigger an OOM in the guest.
guest
.ssh_command("echo f | sudo tee /proc/sysrq-trigger")
.unwrap();
// Give some time for the OOM to happen in the guest and be reported
// back to the host.
assert!(wait_until(Duration::from_secs(20), || {
balloon_size(&api_socket) < 2147483648
}));
// 2nd: check balloon_mem's value to verify balloon has been automatically deflated
let deflated_balloon = balloon_size(&api_socket);
println!("After deflating, balloon memory size is {deflated_balloon} bytes");
// Verify the balloon size deflated
assert!(deflated_balloon < 2147483648);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))] // See #7456
fn test_virtio_balloon_free_page_reporting() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
//Let's start a 4G guest with balloon occupied 2G memory
let mut child = GuestCommand::new(&guest)
.default_cpus()
.args(["--memory", "size=4G"])
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args(["--balloon", "size=0,free_page_reporting=on"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let pid = child.id();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check the initial RSS is less than 1GiB
let rss = process_rss_kib(pid);
println!("RSS {rss} < 1048576");
assert!(rss < 1048576);
// Spawn a command inside the guest to consume 2GiB of RAM for 60
// seconds
let guest_ip = guest.network.guest_ip0.clone();
thread::spawn(move || {
ssh_command_ip(
"stress --vm 1 --vm-bytes 2G --vm-keep --timeout 60",
&guest_ip,
DEFAULT_SSH_RETRIES,
DEFAULT_SSH_TIMEOUT,
)
.unwrap();
});
// Wait for guest memory consumption to reach the expected level.
assert!(wait_until(Duration::from_secs(60), || process_rss_kib(pid) >= 2097152));
let rss = process_rss_kib(pid);
println!("RSS {rss} >= 2097152");
assert!(rss >= 2097152);
// Wait for stress to complete and free-page reporting to shrink RSS again.
assert!(wait_until(Duration::from_secs(120), || process_rss_kib(
pid
) < 2097152));
let rss = process_rss_kib(pid);
println!("RSS {rss} < 2097152");
assert!(rss < 2097152);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_pmem_hotplug() {
_test_pmem_hotplug(None);
}
#[test]
fn test_pmem_multi_segment_hotplug() {
_test_pmem_hotplug(Some(15));
}
fn _test_pmem_hotplug(pci_segment: Option<u16>) {
#[cfg(target_arch = "aarch64")]
let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string();
#[cfg(target_arch = "x86_64")]
let focal_image = FOCAL_IMAGE_NAME.to_string();
let disk_config = UbuntuDiskConfig::new(focal_image);
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);
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_disks()
.default_net()
.capture_output();
if pci_segment.is_some() {
cmd.args([
"--platform",
&format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"),
]);
}
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check /dev/pmem0 is not there
assert_eq!(
guest
.ssh_command("lsblk | grep -c pmem0 || true")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or(1),
0
);
let pmem_temp_file = TempFile::new().unwrap();
pmem_temp_file.as_file().set_len(128 << 20).unwrap();
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-pmem",
Some(&format!(
"file={},id=test0{}",
pmem_temp_file.as_path().to_str().unwrap(),
if let Some(pci_segment) = pci_segment {
format!(",pci_segment={pci_segment}")
} else {
String::new()
}
)),
);
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\"}")
);
}
// Check that /dev/pmem0 exists and the block size is 128M
assert_eq!(
guest
.ssh_command("lsblk | grep pmem0 | grep -c 128M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
guest.reboot_linux(0);
// Check still there after reboot
assert_eq!(
guest
.ssh_command("lsblk | grep pmem0 | grep -c 128M")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or_default(),
1
);
assert!(remote_command(&api_socket, "remove-device", Some("test0")));
// Wait for the pmem device to disappear from lsblk.
assert!(wait_until(Duration::from_secs(20), || {
guest
.ssh_command("lsblk | grep -c pmem0.*128M || true")
.is_ok_and(|output| output.trim().parse::<u32>().unwrap_or(1) == 0)
}));
guest.reboot_linux(1);
// Check still absent after reboot
assert_eq!(
guest
.ssh_command("lsblk | grep -c pmem0.*128M || 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);
}
#[test]
fn test_net_hotplug() {
#[cfg(target_arch = "x86_64")]
let kernel_path = direct_kernel_boot_path();
#[cfg(target_arch = "aarch64")]
let kernel_path = edk2_path();
let guest =
basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel_path(kernel_path.to_str().unwrap());
_test_net_hotplug(&guest, MAX_NUM_PCI_SEGMENTS, None);
}
#[test]
fn test_net_multi_segment_hotplug() {
#[cfg(target_arch = "x86_64")]
let kernel_path = direct_kernel_boot_path();
#[cfg(target_arch = "aarch64")]
let kernel_path = edk2_path();
let guest =
basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel_path(kernel_path.to_str().unwrap());
_test_net_hotplug(&guest, MAX_NUM_PCI_SEGMENTS, Some(15));
}
#[test]
fn test_initramfs() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let mut workload_path = dirs::home_dir().unwrap();
workload_path.push("workloads");
#[cfg(target_arch = "x86_64")]
let mut kernels = vec![direct_kernel_boot_path()];
#[cfg(target_arch = "aarch64")]
let kernels = [direct_kernel_boot_path()];
#[cfg(target_arch = "x86_64")]
{
let mut pvh_kernel_path = workload_path.clone();
pvh_kernel_path.push("vmlinux-x86_64");
kernels.push(pvh_kernel_path);
}
let mut initramfs_path = workload_path;
initramfs_path.push("alpine_initramfs.img");
let test_string = String::from("axz34i9rylotd8n50wbv6kcj7f2qushme1pg");
let cmdline = format!("console=hvc0 quiet TEST_STRING={test_string}");
kernels.iter().for_each(|k_path| {
let mut child = GuestCommand::new(&guest)
.args(["--kernel", k_path.to_str().unwrap()])
.args(["--initramfs", initramfs_path.to_str().unwrap()])
.args(["--cmdline", &cmdline])
.capture_output()
.spawn()
.unwrap();
thread::sleep(std::time::Duration::new(20, 0));
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
let r = std::panic::catch_unwind(|| {
let s = String::from_utf8_lossy(&output.stdout);
assert_ne!(s.lines().position(|line| line == test_string), None);
});
handle_child_output(r, &output);
});
}
#[test]
fn test_counters() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_counters(&guest);
}
#[test]
#[cfg(feature = "guest_debug")]
fn test_coredump() {
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 mut cmd = GuestCommand::new(&guest);
cmd.args(["--cpus", "boot=4"])
.args(["--memory", "size=1G"])
.args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()])
.default_disks()
.args(["--net", guest.default_net_string().as_str()])
.args(["--api-socket", &api_socket])
.capture_output();
let mut child = cmd.spawn().unwrap();
let vmcore_file = temp_vmcore_file_path(&guest.tmp_dir);
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert!(remote_command(&api_socket, "pause", None));
assert!(remote_command(
&api_socket,
"coredump",
Some(format!("file://{vmcore_file}").as_str()),
));
// the num of CORE notes should equals to vcpu
let readelf_core_num_cmd =
format!("readelf --all {vmcore_file} |grep CORE |grep -v Type |wc -l");
let core_num_in_elf = exec_host_command_output(&readelf_core_num_cmd);
assert_eq!(String::from_utf8_lossy(&core_num_in_elf.stdout).trim(), "4");
// the num of QEMU notes should equals to vcpu
let readelf_vmm_num_cmd = format!("readelf --all {vmcore_file} |grep QEMU |wc -l");
let vmm_num_in_elf = exec_host_command_output(&readelf_vmm_num_cmd);
assert_eq!(String::from_utf8_lossy(&vmm_num_in_elf.stdout).trim(), "4");
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(feature = "guest_debug")]
fn test_coredump_no_pause() {
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 mut cmd = GuestCommand::new(&guest);
cmd.args(["--cpus", "boot=4"])
.args(["--memory", "size=1G"])
.args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()])
.default_disks()
.args(["--net", guest.default_net_string().as_str()])
.args(["--api-socket", &api_socket])
.capture_output();
let mut child = cmd.spawn().unwrap();
let vmcore_file = temp_vmcore_file_path(&guest.tmp_dir);
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert!(remote_command(
&api_socket,
"coredump",
Some(format!("file://{vmcore_file}").as_str()),
));
assert_eq!(vm_state(&api_socket), "Running");
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_watchdog() {
let guest = basic_regular_guest!(FOCAL_IMAGE_NAME);
_test_watchdog(&guest);
}
#[test]
fn test_pvpanic() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
_test_pvpanic(&guest);
}
#[test]
fn test_tap_from_fd() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2);
_test_tap_from_fd(&guest);
}
#[test]
#[cfg_attr(target_arch = "aarch64", ignore = "See #5443")]
fn test_macvtap() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2);
_test_macvtap(&guest, false, "guestmacvtap0", "hostmacvtap0");
}
#[test]
#[cfg_attr(target_arch = "aarch64", ignore = "See #5443")]
fn test_macvtap_hotplug() {
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2);
_test_macvtap(&guest, true, "guestmacvtap1", "hostmacvtap1");
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_ovs_dpdk() {
let disk_config1 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
let guest1 = Guest::new(Box::new(disk_config1));
let disk_config2 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
let guest2 = Guest::new(Box::new(disk_config2));
let api_socket_source = format!("{}.1", temp_api_path(&guest2.tmp_dir));
let (mut child1, mut child2) =
setup_ovs_dpdk_guests(&guest1, &guest2, &api_socket_source, false);
// Create the snapshot directory
let snapshot_dir = temp_snapshot_dir_path(&guest2.tmp_dir);
let r = std::panic::catch_unwind(|| {
// Remove one of the two ports from the OVS bridge
assert!(exec_host_command_status("ovs-vsctl del-port vhost-user1").success());
// Spawn a new netcat listener in the first VM
let guest_ip = guest1.network.guest_ip0.clone();
thread::spawn(move || {
ssh_command_ip(
"nc -l 12345",
&guest_ip,
DEFAULT_SSH_RETRIES,
DEFAULT_SSH_TIMEOUT,
)
.unwrap();
});
guest1
.wait_for_ssh_command(
"ss -ltnH | awk '{print $4}' | grep -q ':12345$'",
Duration::from_secs(20),
)
.unwrap();
// Check the connection fails this time
guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap_err();
// Add the OVS port back
assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user1 -- set Interface vhost-user1 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient1").success());
// And finally check the connection is functional again
guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap();
// Pause the VM
assert!(remote_command(&api_socket_source, "pause", None));
// Take a snapshot from the VM
assert!(remote_command(
&api_socket_source,
"snapshot",
Some(format!("file://{snapshot_dir}").as_str()),
));
// Wait for the source VM snapshot artifacts to be ready.
assert!(wait_until(Duration::from_secs(10), || {
std::path::Path::new(&snapshot_dir).exists()
}));
});
// Shutdown the source VM
kill_child(&mut child2);
let output = child2.wait_with_output().unwrap();
handle_child_output(r, &output);
// Remove the vhost-user socket file.
Command::new("rm")
.arg("-f")
.arg("/tmp/dpdkvhostclient2")
.output()
.unwrap();
let api_socket_restored = format!("{}.2", temp_api_path(&guest2.tmp_dir));
// Restore the VM from the snapshot
let mut child2 = GuestCommand::new(&guest2)
.args(["--api-socket", &api_socket_restored])
.args([
"--restore",
format!("source_url=file://{snapshot_dir}").as_str(),
])
.capture_output()
.spawn()
.unwrap();
// Wait for the restored VM to accept SSH again after resume.
let r = std::panic::catch_unwind(|| {
// Resume the VM
assert!(wait_until(Duration::from_secs(30), || remote_command(
&api_socket_restored,
"info",
None
)));
assert!(remote_command(&api_socket_restored, "resume", None));
guest2.wait_for_ssh(Duration::from_secs(30)).unwrap();
// Spawn a new netcat listener in the first VM
let guest_ip = guest1.network.guest_ip0.clone();
thread::spawn(move || {
ssh_command_ip(
"nc -l 12345",
&guest_ip,
DEFAULT_SSH_RETRIES,
DEFAULT_SSH_TIMEOUT,
)
.unwrap();
});
guest1
.wait_for_ssh_command(
"ss -ltnH | awk '{print $4}' | grep -q ':12345$'",
Duration::from_secs(20),
)
.unwrap();
// And check the connection is still functional after restore
guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap();
});
kill_child(&mut child1);
kill_child(&mut child2);
let output = child1.wait_with_output().unwrap();
let output2 = child2.wait_with_output().unwrap();
cleanup_ovs_dpdk();
if r.is_err() {
eprintln!(
"\n\n==== Start restored VM stdout ====\n\n{}\n\n==== End restored VM stdout ====",
String::from_utf8_lossy(&output2.stdout)
);
eprintln!(
"\n\n==== Start restored VM stderr ====\n\n{}\n\n==== End restored VM stderr ====",
String::from_utf8_lossy(&output2.stderr)
);
}
handle_child_output(r, &output);
}
fn setup_spdk_nvme(nvme_dir: &std::path::Path) -> Child {
cleanup_spdk_nvme();
assert!(
exec_host_command_status(&format!(
"mkdir -p {}",
nvme_dir.join("nvme-vfio-user").to_str().unwrap()
))
.success()
);
assert!(
exec_host_command_status(&format!(
"truncate {} -s 128M",
nvme_dir.join("test-disk.raw").to_str().unwrap()
))
.success()
);
assert!(
exec_host_command_status(&format!(
"mkfs.ext4 {}",
nvme_dir.join("test-disk.raw").to_str().unwrap()
))
.success()
);
// Start the SPDK nvmf_tgt daemon to present NVMe device as a VFIO user device
let child = Command::new("/usr/local/bin/spdk-nvme/nvmf_tgt")
.args(["-i", "0", "-m", "0x1"])
.spawn()
.unwrap();
thread::sleep(std::time::Duration::new(2, 0));
assert!(exec_host_command_with_retries(
"/usr/local/bin/spdk-nvme/rpc.py nvmf_create_transport -t VFIOUSER",
3,
std::time::Duration::new(5, 0),
));
assert!(
exec_host_command_status(&format!(
"/usr/local/bin/spdk-nvme/rpc.py bdev_aio_create {} test 512",
nvme_dir.join("test-disk.raw").to_str().unwrap()
))
.success()
);
assert!(exec_host_command_status(
"/usr/local/bin/spdk-nvme/rpc.py nvmf_create_subsystem nqn.2019-07.io.spdk:cnode -a -s test"
)
.success());
assert!(exec_host_command_status(
"/usr/local/bin/spdk-nvme/rpc.py nvmf_subsystem_add_ns nqn.2019-07.io.spdk:cnode test"
)
.success());
assert!(exec_host_command_status(&format!(
"/usr/local/bin/spdk-nvme/rpc.py nvmf_subsystem_add_listener nqn.2019-07.io.spdk:cnode -t VFIOUSER -a {} -s 0",
nvme_dir.join("nvme-vfio-user").to_str().unwrap()
))
.success());
child
}
fn cleanup_spdk_nvme() {
exec_host_command_status("pkill -f nvmf_tgt");
}
#[test]
fn test_vfio_user() {
let jammy_image = JAMMY_IMAGE_NAME.to_string();
let disk_config = UbuntuDiskConfig::new(jammy_image);
let guest = Guest::new(Box::new(disk_config));
let spdk_nvme_dir = guest.tmp_dir.as_path().join("test-vfio-user");
let mut spdk_child = setup_spdk_nvme(spdk_nvme_dir.as_path());
let api_socket = temp_api_path(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket])
.default_cpus()
.args(["--memory", "size=1G,shared=on,hugepages=on"])
.args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()])
.args(["--serial", "tty", "--console", "off"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Hotplug the SPDK-NVMe device to the VM
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-user-device",
Some(&format!(
"socket={},id=vfio_user0",
spdk_nvme_dir
.as_path()
.join("nvme-vfio-user/cntrl")
.to_str()
.unwrap(),
)),
);
assert!(cmd_success);
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"vfio_user0\",\"bdf\":\"0000:00:05.0\"}")
);
// Check both if /dev/nvme exists and if the block size is 128M.
assert!(wait_until(Duration::from_secs(10), || {
guest
.ssh_command("lsblk | grep nvme0n1 | grep -c 128M")
.ok()
.and_then(|output| output.trim().parse::<u32>().ok())
== Some(1)
}));
// Check changes persist after reboot
assert_eq!(
guest.ssh_command("sudo mount /dev/nvme0n1 /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/nvme0n1 /mnt").unwrap(),
""
);
assert_eq!(
guest.ssh_command("sudo cat /mnt/test").unwrap().trim(),
"test123"
);
});
let _ = spdk_child.kill();
let _ = spdk_child.wait();
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_vdpa_block() {
// Before trying to run the test, verify the vdpa_sim_blk module is correctly loaded.
assert!(exec_host_command_status("lsmod | grep vdpa_sim_blk").success());
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2);
_test_vdpa_block(&guest);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_vdpa_net() {
// Before trying to run the test, verify the vdpa_sim_net module is correctly loaded.
if !exec_host_command_status("lsmod | grep vdpa_sim_net").success() {
return;
}
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 mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=2"])
.args(["--memory", "size=512M,hugepages=on"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.default_net()
.args(["--vdpa", "path=/dev/vhost-vdpa-2,num_queues=3"])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check we can find network interface related to vDPA device
assert_eq!(
guest
.ssh_command("ip -o link | grep -c ens6")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or(0),
1
);
guest
.ssh_command("sudo ip link set dev ens6 address 00:e8:ca:33:ba:06")
.unwrap();
guest
.ssh_command("sudo ip addr add 172.16.1.2/24 dev ens6")
.unwrap();
guest.ssh_command("sudo ip link set up dev ens6").unwrap();
// Check there is no packet yet on both TX/RX of the network interface
assert_eq!(
guest
.ssh_command("ip -j -p -s link show ens6 | grep -c '\"packets\": 0'")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or(0),
2
);
// Send 6 packets with ping command
guest.ssh_command("ping 172.16.1.10 -c 6 || true").unwrap();
// Check we can find 6 packets on both TX/RX of the network interface
assert_eq!(
guest
.ssh_command("ip -j -p -s link show ens6 | grep -c '\"packets\": 6'")
.unwrap()
.trim()
.parse::<u32>()
.unwrap_or(0),
2
);
// No need to check for hotplug as we already tested it through
// test_vdpa_block()
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))] // See issue #7439
#[cfg(target_arch = "x86_64")]
fn test_tpm() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let (mut swtpm_command, swtpm_socket_path) = prepare_swtpm_daemon(&guest.tmp_dir);
let mut guest_cmd = GuestCommand::new(&guest);
guest_cmd
.default_cpus()
.args(["--memory", "size=1G"])
.args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()])
.args(["--tpm", &format!("socket={swtpm_socket_path}")])
.capture_output()
.default_disks()
.default_net();
// Start swtpm daemon
let mut swtpm_child = swtpm_command.spawn().unwrap();
assert!(wait_until(Duration::from_secs(10), || {
std::path::Path::new(&swtpm_socket_path).exists()
}));
let mut child = guest_cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(
guest.ssh_command("ls /dev/tpm0").unwrap().trim(),
"/dev/tpm0"
);
guest.ssh_command("sudo tpm2_selftest -f").unwrap();
guest
.ssh_command("echo 'hello' > /tmp/checksum_test; ")
.unwrap();
guest.ssh_command("cmp <(sudo tpm2_pcrevent /tmp/checksum_test | grep sha256 | awk '{print $2}') <(sha256sum /tmp/checksum_test| awk '{print $1}')").unwrap();
});
let _ = swtpm_child.kill();
let _d_out = swtpm_child.wait_with_output().unwrap();
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_double_tty() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let mut cmd = GuestCommand::new(&guest);
let api_socket = temp_api_path(&guest.tmp_dir);
let tty_str: &str = "console=hvc0 earlyprintk=ttyS0 ";
// linux printk module enable console log.
let con_dis_str: &str = "console [hvc0] enabled";
// linux printk module disable console log.
let con_enb_str: &str = "bootconsole [earlyser0] disabled";
let kernel_path = direct_kernel_boot_path();
cmd.default_cpus()
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args([
"--cmdline",
DIRECT_KERNEL_BOOT_CMDLINE
.replace("console=hvc0", tty_str)
.as_str(),
])
.capture_output()
.default_disks()
.default_net()
.args(["--serial", "tty"])
.args(["--console", "tty"])
.args(["--api-socket", &api_socket]);
let mut child = cmd.spawn().unwrap();
let mut r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
if r.is_ok() {
r = std::panic::catch_unwind(|| {
let s = String::from_utf8_lossy(&output.stdout);
assert!(s.contains(tty_str));
assert!(s.contains(con_dis_str));
assert!(s.contains(con_enb_str));
});
}
handle_child_output(r, &output);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_nmi() {
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 kernel_path = direct_kernel_boot_path();
let cmd_line = format!("{} {}", DIRECT_KERNEL_BOOT_CMDLINE, "unknown_nmi_panic=1");
let mut cmd = GuestCommand::new(&guest);
cmd.args(["--cpus", "boot=4"])
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", cmd_line.as_str()])
.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();
assert!(remote_command(&api_socket, "nmi", None));
let expected_sequential_events = [&MetaEvent {
event: "panic".to_string(),
device_id: None,
}];
assert!(wait_until(Duration::from_secs(3), || {
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);
}
#[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);
// Wait for the hotplugged device to appear in the guest
assert!(wait_until(Duration::from_secs(10), || {
ssh_command_ip_with_auth(
&format!("lspci -n | grep \"00:{first_free_device_id:02x}.0\""),
&default_guest_auth(),
&guest.network.guest_ip0,
Some(Duration::from_secs(1)),
)
.is_ok()
}));
// 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!(wait_until(Duration::from_secs(10), || {
matches!(
ssh_command_ip_with_auth(
&format!("lspci -n | grep \"00:{device_id_to_allocate:02x}.0\""),
&default_guest_auth(),
&guest.network.guest_ip0,
Some(Duration::from_secs(5)),
),
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);
// Wait for the hotplugged device to appear in the guest
assert!(wait_until(Duration::from_secs(10), || {
ssh_command_ip_with_auth(
&format!("lspci -n | grep \"00:{allocated_device_id:02x}.0\""),
&default_guest_auth(),
&guest.network.guest_ip0,
Some(Duration::from_secs(1)),
)
.is_ok()
}));
// Remove the first device to create a hole
let cmd_success = remote_command(&api_socket, "remove-device", Some("test0"));
assert!(cmd_success);
// Wait for the device to disappear from the guest
assert!(wait_until(Duration::from_secs(10), || {
matches!(
ssh_command_ip_with_auth(
&format!("lspci -n | grep \"00:{first_free_device_id:02x}.0\""),
&default_guest_auth(),
&guest.network.guest_ip0,
Some(Duration::from_secs(1)),
),
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);
// Wait for the re-added device to appear in the guest
assert!(wait_until(Duration::from_secs(10), || {
ssh_command_ip_with_auth(
&format!("lspci -n | grep \"00:{allocated_device_id:02x}.0\""),
&default_guest_auth(),
&guest.network.guest_ip0,
Some(Duration::from_secs(1)),
)
.is_ok()
}));
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
// Test that adding a duplicate PCI device ID fails
fn test_duplicate_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);
let (cmd_success, _, cmd_stderr) = remote_command_w_output(
&api_socket,
"add-net",
Some(
format!(
"id=test1337,tap=,mac={},ip={},mask=255.255.255.128,pci_device_id={first_free_device_id}",
guest.network.guest_mac1, guest.network.host_ip1,
)
.as_str(),
),
);
// Check for fail; Allocating the same device ID for two devices is disallowed
assert!(!cmd_success);
// Check that the error message contains the expected error
let std_err_str = String::from_utf8(cmd_stderr).unwrap();
assert!(
std_err_str.contains(&format!(
"Valid PCI device identifier but already used: {first_free_device_id}"
)),
"Command return was: {std_err_str}"
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
// Test that requesting an invalid device ID fails.
fn test_invalid_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();
let r = std::panic::catch_unwind(|| {
// Invalid API call because the PCI device ID is out of range
let (cmd_success, _, cmd_stderr) = remote_command_w_output(
&api_socket,
"add-net",
Some(
format!(
"id=test0,tap=,mac={},ip={},mask=255.255.255.128,pci_device_id=188",
guest.network.guest_mac1, guest.network.host_ip1,
)
.as_str(),
),
);
// Check for fail
assert!(!cmd_success);
// Check that the error message contains the expected error
let std_err_str = String::from_utf8(cmd_stderr).unwrap();
assert!(
std_err_str
.contains("Given PCI device ID (188) is out of the supported range of 0..32"),
"Command return was: {std_err_str}",
);
// Use the reserved device ID 0 (root device)
let (cmd_success, _, cmd_stderr) = remote_command_w_output(
&api_socket,
"add-net",
Some(
format!(
"id=test0,tap=,mac={},ip={},mask=255.255.255.128,pci_device_id=0",
guest.network.guest_mac1, guest.network.host_ip1,
)
.as_str(),
),
);
// Check for fail
assert!(!cmd_success);
// Check that the error message contains the expected error
let std_err_str = String::from_utf8(cmd_stderr).unwrap();
assert!(
std_err_str.contains("Given PCI device ID (0) is reserved"),
"Command return was: {std_err_str}"
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
// This test exercises the local live-migration between two Cloud Hypervisor VMs on the
// same host. It ensures the following behaviors:
// 1. The source VM is up and functional (including various virtio-devices are working properly);
// 2. The 'send-migration' and 'receive-migration' command finished successfully;
// 3. The source VM terminated gracefully after live migration;
// 4. The destination VM is functional (including various virtio-devices are working properly) after
// live migration;
// Note: This test does not use vsock as we can't create two identical vsock on the same host.
#[cfg(not(feature = "mshv"))]
fn _test_live_migration(upgrade_test: bool, local: bool, paused: 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 console_text = String::from("On a branch floating down river a cricket, singing.");
let net_id = "net123";
let net_params = format!(
"id={},tap=,mac={},ip={},mask=255.255.255.128",
net_id, guest.network.guest_mac0, guest.network.host_ip0
);
let memory_param: &[&str] = if local {
&["--memory", "size=1500M,shared=on"]
} else {
&["--memory", "size=1500M"]
};
let boot_vcpus = 2;
let max_vcpus = 4;
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 pmem_path = String::from("/dev/pmem0");
// Start the source VM
let src_vm_path = if upgrade_test {
cloud_hypervisor_release_path()
} else {
clh_command("cloud-hypervisor")
};
let src_api_socket = temp_api_path(&guest.tmp_dir);
let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path);
src_vm_cmd
.args([
"--cpus",
format!("boot={boot_vcpus},max={max_vcpus}").as_str(),
])
.args(memory_param)
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", net_params.as_str()])
.args(["--api-socket", &src_api_socket])
.args([
"--pmem",
format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(),
]);
let mut src_child = src_vm_cmd.capture_output().spawn().unwrap();
// Start the destination VM
let mut dest_api_socket = temp_api_path(&guest.tmp_dir);
dest_api_socket.push_str(".dest");
let mut dest_child = GuestCommand::new(&guest)
.args(["--api-socket", &dest_api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Make sure the source VM is functional
// Check the number of vCPUs
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
// Check the guest RAM
assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000);
// Check the guest virtio-devices, e.g. block, rng, console, and net
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
// x86_64: Following what's done in the `test_snapshot_restore`, we need
// to make sure that removing and adding back the virtio-net device does
// not break the live-migration support for virtio-pci.
#[cfg(target_arch = "x86_64")]
{
assert!(remote_command(
&src_api_socket,
"remove-device",
Some(net_id),
));
assert!(wait_until(Duration::from_secs(10), || {
guest.wait_for_ssh(Duration::from_secs(1)).is_err()
}));
// Plug the virtio-net device again
assert!(remote_command(
&src_api_socket,
"add-net",
Some(net_params.as_str()),
));
guest.wait_for_ssh(Duration::from_secs(10)).unwrap();
}
// Start the live-migration
let migration_socket = String::from(
guest
.tmp_dir
.as_path()
.join("live-migration.sock")
.to_str()
.unwrap(),
);
assert!(
start_live_migration(
&migration_socket,
&src_api_socket,
&dest_api_socket,
local,
paused
),
"Unsuccessful command: 'send-migration' or 'receive-migration'."
);
});
// Check and report any errors occurred during the live-migration
if r.is_err() {
print_and_panic(
src_child,
dest_child,
None,
"Error occurred during live-migration",
);
}
// Check the source vm has been terminated successful (give it '3s' to settle)
thread::sleep(std::time::Duration::new(3, 0));
if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) {
print_and_panic(
src_child,
dest_child,
None,
"source VM was not terminated successfully.",
);
}
// Post live-migration check to make sure the destination VM is functional
let r = std::panic::catch_unwind(|| {
// Perform same checks to validate VM has been properly migrated
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000);
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
});
// Clean-up the destination VM and make sure it terminated correctly
let _ = dest_child.kill();
let dest_output = dest_child.wait_with_output().unwrap();
handle_child_output(r, &dest_output);
// Check the destination VM has the expected 'console_text' from its output
let r = std::panic::catch_unwind(|| {
assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text));
});
handle_child_output(r, &dest_output);
}
#[cfg(not(feature = "mshv"))]
fn _test_live_migration_watchdog(upgrade_test: bool, local: bool) {
let disk_config = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
let console_text = String::from("On a branch floating down river a cricket, singing.");
let net_id = "net123";
let net_params = format!(
"id={},tap=,mac={},ip={},mask=255.255.255.128",
net_id, guest.network.guest_mac0, guest.network.host_ip0
);
let memory_param: &[&str] = if local {
&["--memory", "size=1500M,shared=on"]
} else {
&["--memory", "size=1500M"]
};
let boot_vcpus = 2;
let max_vcpus = 4;
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 pmem_path = String::from("/dev/pmem0");
// Start the source VM
let src_vm_path = if upgrade_test {
cloud_hypervisor_release_path()
} else {
clh_command("cloud-hypervisor")
};
let src_api_socket = temp_api_path(&guest.tmp_dir);
let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path);
src_vm_cmd
.args([
"--cpus",
format!("boot={boot_vcpus},max={max_vcpus}").as_str(),
])
.args(memory_param)
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", net_params.as_str()])
.args(["--api-socket", &src_api_socket])
.args([
"--pmem",
format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(),
])
.args(["--watchdog"]);
let mut src_child = src_vm_cmd.capture_output().spawn().unwrap();
// Start the destination VM
let mut dest_api_socket = temp_api_path(&guest.tmp_dir);
dest_api_socket.push_str(".dest");
let mut dest_child = GuestCommand::new(&guest)
.args(["--api-socket", &dest_api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Make sure the source VM is functional
// Check the number of vCPUs
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
// Check the guest RAM
assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000);
// Check the guest virtio-devices, e.g. block, rng, console, and net
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
// x86_64: Following what's done in the `test_snapshot_restore`, we need
// to make sure that removing and adding back the virtio-net device does
// not break the live-migration support for virtio-pci.
#[cfg(target_arch = "x86_64")]
{
assert!(remote_command(
&src_api_socket,
"remove-device",
Some(net_id),
));
assert!(wait_until(Duration::from_secs(10), || {
guest.wait_for_ssh(Duration::from_secs(1)).is_err()
}));
// Plug the virtio-net device again
assert!(remote_command(
&src_api_socket,
"add-net",
Some(net_params.as_str()),
));
guest.wait_for_ssh(Duration::from_secs(10)).unwrap();
}
// Enable watchdog and ensure its functional
let 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);
// Start the live-migration
let migration_socket = String::from(
guest
.tmp_dir
.as_path()
.join("live-migration.sock")
.to_str()
.unwrap(),
);
assert!(
start_live_migration(
&migration_socket,
&src_api_socket,
&dest_api_socket,
local,
false
),
"Unsuccessful command: 'send-migration' or 'receive-migration'."
);
});
// Check and report any errors occurred during the live-migration
if r.is_err() {
print_and_panic(
src_child,
dest_child,
None,
"Error occurred during live-migration",
);
}
// Check the source vm has been terminated successful (give it '3s' to settle)
thread::sleep(std::time::Duration::new(3, 0));
if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) {
print_and_panic(
src_child,
dest_child,
None,
"source VM was not terminated successfully.",
);
}
// Post live-migration check to make sure the destination VM is functional
let r = std::panic::catch_unwind(|| {
// Perform same checks to validate VM has been properly migrated
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000);
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
// Perform checks on watchdog
let mut expected_reboot_count = 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(50).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(&dest_api_socket, "pause", None));
thread::sleep(std::time::Duration::new(30, 0));
assert!(remote_command(&dest_api_socket, "resume", None));
// Check no reboot
assert_eq!(get_reboot_count(&guest), expected_reboot_count);
}
});
// Clean-up the destination VM and make sure it terminated correctly
let _ = dest_child.kill();
let dest_output = dest_child.wait_with_output().unwrap();
handle_child_output(r, &dest_output);
// Check the destination VM has the expected 'console_text' from its output
let r = std::panic::catch_unwind(|| {
assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text));
});
handle_child_output(r, &dest_output);
}
// This test exercises the local live-migration between two Cloud Hypervisor VMs on the
// same host with Landlock enabled on both VMs. The test validates the following:
// 1. The source VM is up and functional
// 2. Ensure Landlock is enabled on source VM by hotplugging a disk. As the path for this
// disk is not known to the source VM this step will fail.
// 3. The 'send-migration' and 'receive-migration' command finished successfully;
// 4. The source VM terminated gracefully after live migration;
// 5. The destination VM is functional after live migration;
// 6. Ensure Landlock is enabled on destination VM by hotplugging a disk. As the path for
// this disk is not known to the destination VM this step will fail.
#[cfg(not(feature = "mshv"))]
fn _test_live_migration_with_landlock() {
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 net_id = "net123";
let net_params = format!(
"id={},tap=,mac={},ip={},mask=255.255.255.128",
net_id, guest.network.guest_mac0, guest.network.host_ip0
);
let boot_vcpus = 2;
let max_vcpus = 4;
let mut blk_file_path = dirs::home_dir().unwrap();
blk_file_path.push("workloads");
blk_file_path.push("blk.img");
let src_api_socket = temp_api_path(&guest.tmp_dir);
let mut src_child = GuestCommand::new(&guest)
.args([
"--cpus",
format!("boot={boot_vcpus},max={max_vcpus}").as_str(),
])
.args(["--memory", "size=1500M,shared=on"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--api-socket", &src_api_socket])
.args(["--landlock"])
.args(["--net", net_params.as_str()])
.args([
"--landlock-rules",
format!("path={:?},access=rw", guest.tmp_dir.as_path()).as_str(),
])
.capture_output()
.spawn()
.unwrap();
// Start the destination VM
let mut dest_api_socket = temp_api_path(&guest.tmp_dir);
dest_api_socket.push_str(".dest");
let mut dest_child = GuestCommand::new(&guest)
.args(["--api-socket", &dest_api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Make sure the source VM is functaionl
// Check the number of vCPUs
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
// Check the guest RAM
assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000);
// Check Landlock is enabled by hot-plugging a disk.
assert!(!remote_command(
&src_api_socket,
"add-disk",
Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()),
));
// Start the live-migration
let migration_socket = String::from(
guest
.tmp_dir
.as_path()
.join("live-migration.sock")
.to_str()
.unwrap(),
);
assert!(
start_live_migration(
&migration_socket,
&src_api_socket,
&dest_api_socket,
true,
false
),
"Unsuccessful command: 'send-migration' or 'receive-migration'."
);
});
// Check and report any errors occurred during the live-migration
if r.is_err() {
print_and_panic(
src_child,
dest_child,
None,
"Error occurred during live-migration",
);
}
// Check the source vm has been terminated successful (give it '3s' to settle)
thread::sleep(std::time::Duration::new(3, 0));
if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) {
print_and_panic(
src_child,
dest_child,
None,
"source VM was not terminated successfully.",
);
}
// Post live-migration check to make sure the destination VM is functioning
let r = std::panic::catch_unwind(|| {
// Perform same checks to validate VM has been properly migrated
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000);
});
// Check Landlock is enabled on destination VM by hot-plugging a disk.
assert!(!remote_command(
&dest_api_socket,
"add-disk",
Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()),
));
// Clean-up the destination VM and make sure it terminated correctly
let _ = dest_child.kill();
let dest_output = dest_child.wait_with_output().unwrap();
handle_child_output(r, &dest_output);
}
// Function to get an available port
#[cfg(not(feature = "mshv"))]
fn get_available_port() -> u16 {
TcpListener::bind("127.0.0.1:0")
.expect("Failed to bind to address")
.local_addr()
.unwrap()
.port()
}
#[cfg(not(feature = "mshv"))]
fn start_live_migration_tcp(
src_api_socket: &str,
dest_api_socket: &str,
connections: NonZeroU32,
) -> bool {
// Get an available TCP port
let migration_port = get_available_port();
let host_ip = "127.0.0.1";
// Start the 'receive-migration' command on the destination
let mut receive_migration = Command::new(clh_command("ch-remote"))
.args([
&format!("--api-socket={dest_api_socket}"),
"receive-migration",
&format!("tcp:0.0.0.0:{migration_port}"),
])
.stdin(Stdio::null())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
// Give the destination some time to start listening
thread::sleep(Duration::from_secs(1));
// Start the 'send-migration' command on the source
let connections = connections.get();
let mut send_migration = Command::new(clh_command("ch-remote"))
.args([
&format!("--api-socket={src_api_socket}"),
"send-migration",
&format!(
"destination_url=tcp:{host_ip}:{migration_port},connections={connections}"
),
])
.stdin(Stdio::null())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
// Check if the 'send-migration' command executed successfully
let send_success = if let Some(status) = send_migration
.wait_timeout(Duration::from_secs(60))
.unwrap()
{
status.success()
} else {
false
};
if !send_success {
let _ = send_migration.kill();
let output = send_migration.wait_with_output().unwrap();
eprintln!(
"\n\n==== Start 'send_migration' output ====\n\n---stdout---\n{}\n\n---stderr---\n{}\n\n==== End 'send_migration' output ====\n\n",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
// Check if the 'receive-migration' command executed successfully
let receive_success = if let Some(status) = receive_migration
.wait_timeout(Duration::from_secs(60))
.unwrap()
{
status.success()
} else {
false
};
if !receive_success {
let _ = receive_migration.kill();
let output = receive_migration.wait_with_output().unwrap();
eprintln!(
"\n\n==== Start 'receive_migration' output ====\n\n---stdout---\n{}\n\n---stderr---\n{}\n\n==== End 'receive_migration' output ====\n\n",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
send_success && receive_success
}
#[cfg(not(feature = "mshv"))]
fn _test_live_migration_tcp(connections: NonZeroU32) {
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 console_text = String::from("On a branch floating down river a cricket, singing.");
let net_id = "net123";
let net_params = format!(
"id={},tap=,mac={},ip={},mask=255.255.255.128",
net_id, guest.network.guest_mac0, guest.network.host_ip0
);
let memory_param: &[&str] = &["--memory", "size=1500M,shared=on"];
let boot_vcpus = 2;
let max_vcpus = 4;
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 pmem_path = String::from("/dev/pmem0");
// Start the source VM
let src_vm_path = clh_command("cloud-hypervisor");
let src_api_socket = temp_api_path(&guest.tmp_dir);
let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path);
src_vm_cmd
.args([
"--cpus",
format!("boot={boot_vcpus},max={max_vcpus}").as_str(),
])
.args(memory_param)
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", net_params.as_str()])
.args(["--api-socket", &src_api_socket])
.args([
"--pmem",
format!(
"file={},discard_writes=on",
pmem_temp_file.as_path().to_str().unwrap(),
)
.as_str(),
])
.capture_output();
let mut src_child = src_vm_cmd.spawn().unwrap();
// Start the destination VM
let mut dest_api_socket = temp_api_path(&guest.tmp_dir);
dest_api_socket.push_str(".dest");
let mut dest_child = GuestCommand::new(&guest)
.args(["--api-socket", &dest_api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Ensure the source VM is running normally
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000);
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
// On x86_64 architecture, remove and re-add the virtio-net device
#[cfg(target_arch = "x86_64")]
{
assert!(remote_command(
&src_api_socket,
"remove-device",
Some(net_id),
));
assert!(wait_until(Duration::from_secs(10), || {
guest.wait_for_ssh(Duration::from_secs(1)).is_err()
}));
// Re-add the virtio-net device
assert!(remote_command(
&src_api_socket,
"add-net",
Some(net_params.as_str()),
));
guest.wait_for_ssh(Duration::from_secs(10)).unwrap();
}
// Start TCP live migration
assert!(
start_live_migration_tcp(&src_api_socket, &dest_api_socket, connections),
"Unsuccessful command: 'send-migration' or 'receive-migration'."
);
});
// Check and report any errors that occurred during live migration
if r.is_err() {
print_and_panic(
src_child,
dest_child,
None,
"Error occurred during live-migration",
);
}
// Check the source vm has been terminated successful (give it '3s' to settle)
thread::sleep(std::time::Duration::new(3, 0));
if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) {
print_and_panic(
src_child,
dest_child,
None,
"Source VM was not terminated successfully.",
);
}
// After live migration, ensure the destination VM is running normally
let r = std::panic::catch_unwind(|| {
// Perform the same checks to ensure the VM has migrated correctly
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000);
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
});
// Clean up the destination VM and ensure it terminates properly
let _ = dest_child.kill();
let dest_output = dest_child.wait_with_output().unwrap();
handle_child_output(r, &dest_output);
// Check if the expected `console_text` is present in the destination VM's output
let r = std::panic::catch_unwind(|| {
assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text));
});
handle_child_output(r, &dest_output);
}
#[cfg(not(feature = "mshv"))]
fn _test_live_migration_tcp_timeout(timeout_strategy: TimeoutStrategy) {
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 net_id = "net1337";
let net_params = format!(
"id={},tap=,mac={},ip={},mask=255.255.255.128",
net_id, guest.network.guest_mac0, guest.network.host_ip0
);
let memory_param: &[&str] = &["--memory", "size=1500M,shared=on"];
let boot_vcpus = 2;
let src_vm_path = clh_command("cloud-hypervisor");
let src_api_socket = temp_api_path(&guest.tmp_dir);
let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path);
src_vm_cmd
.args(["--cpus", format!("boot={boot_vcpus}").as_str()])
.args(memory_param)
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", net_params.as_str()])
.args(["--api-socket", &src_api_socket])
.capture_output();
let mut src_child = src_vm_cmd.spawn().unwrap();
let mut dest_api_socket = temp_api_path(&guest.tmp_dir);
dest_api_socket.push_str(".dest");
let mut dest_child = GuestCommand::new(&guest)
.args(["--api-socket", &dest_api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
// Start a memory stressor in the background to keep pages dirty,
// ensuring the precopy loop cannot converge within the 1s timeout.
guest
.ssh_command("nohup stress --vm 2 --vm-bytes 220M --vm-keep &>/dev/null &")
.unwrap();
// Give stress a moment to actually start dirtying memory
thread::sleep(Duration::from_secs(3));
let migration_port = get_available_port();
let host_ip = "127.0.0.1";
let mut receive_migration = Command::new(clh_command("ch-remote"))
.args([
&format!("--api-socket={dest_api_socket}"),
"receive-migration",
&format!("tcp:0.0.0.0:{migration_port}"),
])
.stdin(Stdio::null())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
thread::sleep(Duration::from_secs(1));
// Use a tight downtime budget (1ms) combined with a 1s timeout so the
// migration practically cannot converge regardless of strategy.
let mut send_migration = Command::new(clh_command("ch-remote"))
.args([
&format!("--api-socket={src_api_socket}"),
"send-migration",
&format!(
"destination_url=tcp:{host_ip}:{migration_port},downtime_ms=1,timeout_s=1,timeout_strategy={timeout_strategy:?}"
),
])
.stdin(Stdio::null())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let send_status = send_migration
.wait_timeout(Duration::from_secs(60))
.unwrap();
let receive_status = receive_migration
.wait_timeout(Duration::from_secs(60))
.unwrap();
// Clean up receive-migration regardless of its outcome
if receive_status.is_none() {
let _ = receive_migration.kill();
}
// Kill the stressor now that migration has completed or aborted,
// to reduce system load during post-migration checks.
let _ = guest.ssh_command("pkill -f 'stress --vm'");
match timeout_strategy {
TimeoutStrategy::Cancel => {
// With cancel strategy the send must fail and the source VM
// must keep running.
let send_failed = match send_status {
Some(status) => !status.success(),
None => {
let _ = send_migration.kill();
false
}
};
assert!(
send_failed,
"send-migration should have failed due to 1s timeout with cancel strategy"
);
thread::sleep(Duration::from_secs(2));
assert!(
src_child.try_wait().unwrap().is_none(),
"Source VM should still be running after a cancelled migration"
);
// Confirm the source VM is still responsive over SSH
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
}
TimeoutStrategy::Ignore => {
// With Ignore strategy the send must succeed despite the timeout
// being reached, and the source VM must have terminated.
let send_succeeded = match send_status {
Some(status) => status.success(),
None => {
let _ = send_migration.kill();
false
}
};
assert!(
send_succeeded,
"send-migration should have succeeded with timeout_strategy=ignore"
);
thread::sleep(Duration::from_secs(3));
assert!(
src_child.try_wait().unwrap().is_some(),
"Source VM should have terminated after a forced migration"
);
// Confirm the VM is still responsive over SSH on the new host
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
}
}
}));
let _ = src_child.kill();
let src_output = src_child.wait_with_output().unwrap();
let _ = dest_child.kill();
let _dest_output = dest_child.wait_with_output().unwrap();
handle_child_output(r, &src_output);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_basic() {
_test_live_migration(false, false, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_local() {
_test_live_migration(false, true, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_basic_paused() {
_test_live_migration(false, false, true);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_local_paused() {
_test_live_migration(false, true, true);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_tcp() {
_test_live_migration_tcp(NonZeroU32::new(1).unwrap());
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_tcp_parallel_connections() {
_test_live_migration_tcp(NonZeroU32::new(8).unwrap());
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_tcp_timeout_cancel() {
_test_live_migration_tcp_timeout(TimeoutStrategy::Cancel);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_tcp_timeout_ignore() {
_test_live_migration_tcp_timeout(TimeoutStrategy::Ignore);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_watchdog() {
_test_live_migration_watchdog(false, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_watchdog_local() {
_test_live_migration_watchdog(false, true);
}
// TODO: Add test of live upgrade paused vm after cloud-hypervisor-static
// version is updated.
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_upgrade_basic() {
_test_live_migration(true, false, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_upgrade_local() {
_test_live_migration(true, true, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_upgrade_watchdog() {
_test_live_migration_watchdog(true, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_upgrade_watchdog_local() {
_test_live_migration_watchdog(true, true);
}
#[test]
#[cfg(not(feature = "mshv"))]
#[cfg(target_arch = "x86_64")]
fn test_live_migration_with_landlock() {
_test_live_migration_with_landlock();
}
#[cfg(not(feature = "mshv"))]
fn _test_live_migration_virtio_fs(local: 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 shared_dir = guest.tmp_dir.as_path().join("virtiofs_shared");
std::fs::create_dir(&shared_dir).unwrap();
let (daemon_child, virtiofsd_socket_path) =
prepare_virtiofsd(&guest.tmp_dir, shared_dir.to_str().unwrap());
let src_api_socket = temp_api_path(&guest.tmp_dir);
// Start the source VM
let mut src_child = GuestCommand::new(&guest)
.args(["--api-socket", &src_api_socket])
.args(["--cpus", "boot=2"])
.args(["--memory", "size=512M,shared=on"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.default_net()
.args([
"--fs",
format!("socket={virtiofsd_socket_path},tag=myfs,num_queues=1,queue_size=1024")
.as_str(),
])
.capture_output()
.spawn()
.unwrap();
// Start the destination VM
let mut dest_api_socket = temp_api_path(&guest.tmp_dir);
dest_api_socket.push_str(".dest");
let mut dest_child = GuestCommand::new(&guest)
.args(["--api-socket", &dest_api_socket])
.capture_output()
.spawn()
.unwrap();
// Spawn a thread that waits for the old virtiofsd to exit then
// starts a replacement. During migration the source saves
// DEVICE_STATE then disconnects, causing virtiofsd to exit.
// The destination needs a fresh virtiofsd to load DEVICE_STATE.
// We remove the socket file first so the destination cannot
// accidentally connect to the old instance.
let virtiofsd_socket_clone = virtiofsd_socket_path.clone();
let shared_dir_str = shared_dir.to_str().unwrap().to_string();
let (restart_tx, restart_rx) = std::sync::mpsc::channel();
let _monitor = thread::spawn(move || {
let mut child = daemon_child;
let _ = child.wait();
let mut path = dirs::home_dir().unwrap();
path.push("workloads");
path.push("virtiofsd");
let new_child = Command::new(path)
.args(["--shared-dir", &shared_dir_str])
.args(["--socket-path", &virtiofsd_socket_clone])
.args(["--cache", "never"])
.args(["--tag", "myfs"])
.spawn()
.unwrap();
wait_for_virtiofsd_socket(&virtiofsd_socket_clone);
let _ = restart_tx.send(new_child);
});
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Mount virtiofs and verify it works
guest
.ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/")
.unwrap();
// Write a test file through virtiofs before migration
guest
.ssh_command(
"sudo bash -c 'echo pre_migration_data > mount_dir/migration_test_file'",
)
.unwrap();
// Verify the file is accessible
assert_eq!(
guest
.ssh_command("cat mount_dir/migration_test_file")
.unwrap()
.trim(),
"pre_migration_data"
);
let migration_socket = String::from(
guest
.tmp_dir
.as_path()
.join("live-migration.sock")
.to_str()
.unwrap(),
);
// Remove the socket so the destination cannot connect to
// the old virtiofsd (which is still running). The source's
// existing connection uses an already-accepted fd.
let _ = std::fs::remove_file(&virtiofsd_socket_path);
assert!(
start_live_migration(
&migration_socket,
&src_api_socket,
&dest_api_socket,
local,
false
),
"Unsuccessful command: 'send-migration' or 'receive-migration'."
);
});
// Check and report any errors occurred during the live-migration
if r.is_err() {
print_and_panic(
src_child,
dest_child,
None,
"Error occurred during live-migration with virtio-fs",
);
}
// Check the source vm has been terminated successfully (give it '3s' to settle)
thread::sleep(Duration::from_secs(3));
if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) {
print_and_panic(
src_child,
dest_child,
None,
"source VM was not terminated successfully.",
);
}
// Post live-migration checks
let r = std::panic::catch_unwind(|| {
// Verify virtiofs still works after migration
// Read the file written before migration
assert_eq!(
guest
.ssh_command("cat mount_dir/migration_test_file")
.unwrap()
.trim(),
"pre_migration_data"
);
// Write a new file after migration
guest
.ssh_command(
"sudo bash -c 'echo post_migration_data > mount_dir/post_migration_file'",
)
.unwrap();
// Verify the new file exists on the host
let post_content =
std::fs::read_to_string(shared_dir.join("post_migration_file")).unwrap();
assert_eq!(post_content.trim(), "post_migration_data");
});
// Clean up
let _ = dest_child.kill();
let dest_output = dest_child.wait_with_output().unwrap();
if let Ok(mut new_daemon) = restart_rx.try_recv() {
let _ = new_daemon.kill();
let _ = new_daemon.wait();
}
let _ = std::fs::remove_file(shared_dir.join("migration_test_file"));
let _ = std::fs::remove_file(shared_dir.join("post_migration_file"));
handle_child_output(r, &dest_output);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_virtio_fs() {
_test_live_migration_virtio_fs(false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_virtio_fs_local() {
_test_live_migration_virtio_fs(true);
}
}
mod dbus_api {
use crate::*;
// Start cloud-hypervisor with no VM parameters, running both the HTTP
// and DBus APIs. Alternate calls to the external APIs (HTTP and DBus)
// to create a VM, boot it, and verify that it can be shut down and then
// booted again.
#[test]
fn test_api_dbus_and_http_interleaved() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let dbus_api = TargetApi::new_dbus_api(&guest.tmp_dir);
let http_api = TargetApi::new_http_api(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest)
.args(dbus_api.guest_args())
.args(http_api.guest_args())
.capture_output()
.spawn()
.unwrap();
thread::sleep(std::time::Duration::new(1, 0));
// Verify API servers are running
assert!(dbus_api.remote_command("ping", None));
assert!(http_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(|| {
// Create the VM
assert!(dbus_api.remote_command("create", Some(create_config),));
// Then boot it
assert!(http_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 shutdown the VM
assert!(dbus_api.remote_command("shutdown", None));
// Then boot it again
assert!(http_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);
}
#[test]
fn test_api_dbus_create_boot() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = GuestFactory::new_regular_guest_factory()
.create_guest(Box::new(disk_config))
.with_cpu(4);
let target_api = TargetApi::new_dbus_api(&guest.tmp_dir);
_test_api_create_boot(&target_api, &guest);
}
#[test]
fn test_api_dbus_shutdown() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = GuestFactory::new_regular_guest_factory()
.create_guest(Box::new(disk_config))
.with_cpu(4);
let target_api = TargetApi::new_dbus_api(&guest.tmp_dir);
_test_api_shutdown(&target_api, &guest);
}
#[test]
fn test_api_dbus_delete() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = GuestFactory::new_regular_guest_factory()
.create_guest(Box::new(disk_config))
.with_cpu(4);
let target_api = TargetApi::new_dbus_api(&guest.tmp_dir);
_test_api_delete(&target_api, &guest);
}
#[test]
fn test_api_dbus_pause_resume() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = GuestFactory::new_regular_guest_factory()
.create_guest(Box::new(disk_config))
.with_cpu(4);
let target_api = TargetApi::new_dbus_api(&guest.tmp_dir);
_test_api_pause_resume(&target_api, &guest);
}
}
mod ivshmem {
#[cfg(not(feature = "mshv"))]
use std::fs::remove_dir_all;
use std::process::Command;
use test_infra::{Guest, GuestCommand, UbuntuDiskConfig, handle_child_output, kill_child};
use crate::*;
#[cfg(not(feature = "mshv"))]
fn _test_live_migration_ivshmem(local: 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 console_text = String::from("On a branch floating down river a cricket, singing.");
let net_id = "net123";
let net_params = format!(
"id={},tap=,mac={},ip={},mask=255.255.255.128",
net_id, guest.network.guest_mac0, guest.network.host_ip0
);
let memory_param: &[&str] = if local {
&["--memory", "size=4G,shared=on"]
} else {
&["--memory", "size=4G"]
};
let boot_vcpus = 2;
let max_vcpus = 4;
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 pmem_path = String::from("/dev/pmem0");
let ivshmem_file_path = String::from(
guest
.tmp_dir
.as_path()
.join("ivshmem.data")
.to_str()
.unwrap(),
);
let file_size = "1M";
// Create a file to be used as the shared memory
Command::new("dd")
.args([
"if=/dev/zero",
format!("of={ivshmem_file_path}").as_str(),
format!("bs={file_size}").as_str(),
"count=1",
])
.status()
.unwrap();
// Start the source VM
let src_vm_path = clh_command("cloud-hypervisor");
let src_api_socket = temp_api_path(&guest.tmp_dir);
let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path);
src_vm_cmd
.args([
"--cpus",
format!("boot={boot_vcpus},max={max_vcpus}").as_str(),
])
.args(memory_param)
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", net_params.as_str()])
.args(["--api-socket", &src_api_socket])
.args([
"--pmem",
format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(),
])
.args([
"--ivshmem",
format!("path={ivshmem_file_path},size={file_size}").as_str(),
]);
let mut src_child = src_vm_cmd.capture_output().spawn().unwrap();
// Start the destination VM
let mut dest_api_socket = temp_api_path(&guest.tmp_dir);
dest_api_socket.push_str(".dest");
let mut dest_child = GuestCommand::new(&guest)
.args(["--api-socket", &dest_api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Make sure the source VM is functional
// Check the number of vCPUs
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
// Check the guest RAM
assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000);
// Check the guest virtio-devices, e.g. block, rng, console, and net
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
// x86_64: Following what's done in the `test_snapshot_restore`, we need
// to make sure that removing and adding back the virtio-net device does
// not break the live-migration support for virtio-pci.
#[cfg(target_arch = "x86_64")]
{
assert!(remote_command(
&src_api_socket,
"remove-device",
Some(net_id),
));
thread::sleep(Duration::new(10, 0));
// Plug the virtio-net device again
assert!(remote_command(
&src_api_socket,
"add-net",
Some(net_params.as_str()),
));
thread::sleep(Duration::new(10, 0));
}
// Check ivshmem device in src guest.
_test_ivshmem(&guest, &ivshmem_file_path, file_size);
// Allow some normal time to elapse to check we don't get spurious reboots
thread::sleep(std::time::Duration::new(40, 0));
// Start the live-migration
let migration_socket = String::from(
guest
.tmp_dir
.as_path()
.join("live-migration.sock")
.to_str()
.unwrap(),
);
assert!(
start_live_migration(
&migration_socket,
&src_api_socket,
&dest_api_socket,
local,
false
),
"Unsuccessful command: 'send-migration' or 'receive-migration'."
);
});
// Check and report any errors occurred during the live-migration
if r.is_err() {
print_and_panic(
src_child,
dest_child,
None,
"Error occurred during live-migration",
);
}
// Check the source vm has been terminated successful (give it '3s' to settle)
thread::sleep(std::time::Duration::new(3, 0));
if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) {
print_and_panic(
src_child,
dest_child,
None,
"source VM was not terminated successfully.",
);
}
// Post live-migration check to make sure the destination VM is functional
let r = std::panic::catch_unwind(|| {
// Perform same checks to validate VM has been properly migrated
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000);
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
// Check ivshmem device
_test_ivshmem(&guest, &ivshmem_file_path, file_size);
});
// Clean-up the destination VM and make sure it terminated correctly
let _ = dest_child.kill();
let dest_output = dest_child.wait_with_output().unwrap();
handle_child_output(r, &dest_output);
// Check the destination VM has the expected 'console_text' from its output
let r = std::panic::catch_unwind(|| {
assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text));
});
handle_child_output(r, &dest_output);
}
#[test]
fn test_ivshmem() {
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 ivshmem_file_path = String::from(
guest
.tmp_dir
.as_path()
.join("ivshmem.data")
.to_str()
.unwrap(),
);
let file_size = "1M";
// Create a file to be used as the shared memory
Command::new("dd")
.args([
"if=/dev/zero",
format!("of={ivshmem_file_path}").as_str(),
format!("bs={file_size}").as_str(),
"count=1",
])
.status()
.unwrap();
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=2"])
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.default_net()
.args([
"--ivshmem",
format!("path={ivshmem_file_path},size={file_size}").as_str(),
])
.args(["--api-socket", &api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
_test_ivshmem(&guest, &ivshmem_file_path, file_size);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_snapshot_restore_ivshmem() {
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 api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir));
let ivshmem_file_path = String::from(
guest
.tmp_dir
.as_path()
.join("ivshmem.data")
.to_str()
.unwrap(),
);
let file_size = "1M";
// Create a file to be used as the shared memory
Command::new("dd")
.args([
"if=/dev/zero",
format!("of={ivshmem_file_path}").as_str(),
format!("bs={file_size}").as_str(),
"count=1",
])
.status()
.unwrap();
let socket = temp_vsock_path(&guest.tmp_dir);
let event_path = temp_event_monitor_path(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_source])
.args(["--event-monitor", format!("path={event_path}").as_str()])
.args(["--cpus", "boot=2"])
.args(["--memory", "size=1G"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.default_disks()
.default_net()
.args(["--vsock", format!("cid=3,socket={socket}").as_str()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args([
"--ivshmem",
format!("path={ivshmem_file_path},size={file_size}").as_str(),
])
.capture_output()
.spawn()
.unwrap();
let console_text = String::from("On a branch floating down river a cricket, singing.");
// Create the snapshot directory
let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir);
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check the number of vCPUs
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2);
snapshot_restore_common::snapshot_and_check_events(
&api_socket_source,
&snapshot_dir,
&event_path,
);
});
// Shutdown the source VM and check console output
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
// Remove the vsock socket file.
Command::new("rm")
.arg("-f")
.arg(socket.as_str())
.output()
.unwrap();
let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir));
let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir));
// Restore the VM from the snapshot
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_restored])
.args([
"--event-monitor",
format!("path={event_path_restored}").as_str(),
])
.args([
"--restore",
format!("source_url=file://{snapshot_dir}").as_str(),
])
.capture_output()
.spawn()
.unwrap();
let latest_events = [&MetaEvent {
event: "restored".to_string(),
device_id: None,
}];
// Wait for the restored event to show up in the monitor file.
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
// Remove the snapshot dir
let _ = remove_dir_all(snapshot_dir.as_str());
let r = std::panic::catch_unwind(|| {
// Resume the VM
assert!(wait_until(Duration::from_secs(30), || remote_command(
&api_socket_restored,
"info",
None
)));
assert!(remote_command(&api_socket_restored, "resume", None));
let latest_events = [
&MetaEvent {
event: "resuming".to_string(),
device_id: None,
},
&MetaEvent {
event: "resumed".to_string(),
device_id: None,
},
];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
// Check the number of vCPUs
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2);
guest.check_devices_common(Some(&socket), Some(&console_text), None);
_test_ivshmem(&guest, &ivshmem_file_path, file_size);
});
// Shutdown the target VM and check console output
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(&console_text));
});
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_ivshmem() {
_test_live_migration_ivshmem(false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_ivshmem_local() {
_test_live_migration_ivshmem(true);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_snapshot_restore_hotplug_virtiomem() {
snapshot_restore_common::_test_snapshot_restore(true, false);
}
#[test]
#[cfg(not(feature = "mshv"))] // See issue #7437
fn test_snapshot_restore_basic() {
snapshot_restore_common::_test_snapshot_restore(false, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_snapshot_restore_with_resume() {
snapshot_restore_common::_test_snapshot_restore(false, true);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_snapshot_restore_uffd() {
snapshot_restore_common::_test_snapshot_restore_uffd("size=2G", &[], 1_920_000);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_snapshot_restore_uffd_shared_memory() {
snapshot_restore_common::_test_snapshot_restore_uffd("size=512M,shared=on", &[], 480_000);
}
#[test]
#[cfg(not(feature = "mshv"))] // See issue #7437
#[cfg(target_arch = "x86_64")]
fn test_snapshot_restore_pvpanic() {
snapshot_restore_common::_test_snapshot_restore_devices(true);
}
#[test]
fn test_virtio_pmem_persist_writes() {
test_virtio_pmem(false, false);
}
}
#[cfg(not(feature = "mshv"))]
mod snapshot_restore_common {
use std::fs::remove_dir_all;
use std::process::Command;
use crate::*;
pub(crate) fn snapshot_and_check_events(
api_socket: &str,
snapshot_dir: &str,
event_path: &str,
) {
// Pause the VM
assert!(remote_command(api_socket, "pause", None));
let latest_events: [&MetaEvent; 2] = [
&MetaEvent {
event: "pausing".to_string(),
device_id: None,
},
&MetaEvent {
event: "paused".to_string(),
device_id: None,
},
];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, event_path)
}));
// Take a snapshot from the VM
assert!(remote_command(
api_socket,
"snapshot",
Some(format!("file://{snapshot_dir}").as_str()),
));
let latest_events = [
&MetaEvent {
event: "snapshotting".to_string(),
device_id: None,
},
&MetaEvent {
event: "snapshotted".to_string(),
device_id: None,
},
];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, event_path)
}));
}
pub(crate) fn _test_snapshot_restore(use_hotplug: bool, use_resume_option: 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 api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir));
let net_id = "net123";
let net_params = format!(
"id={},tap=,mac={},ip={},mask=255.255.255.128",
net_id, guest.network.guest_mac0, guest.network.host_ip0
);
let mut mem_params = "size=1G";
if use_hotplug {
mem_params = "size=2G,hotplug_method=virtio-mem,hotplug_size=32G";
}
let cloudinit_params = format!(
"path={},iommu=on",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
);
let socket = temp_vsock_path(&guest.tmp_dir);
let event_path = temp_event_monitor_path(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_source])
.args(["--event-monitor", format!("path={event_path}").as_str()])
.args(["--cpus", "boot=4"])
.args(["--memory", mem_params])
.args(["--balloon", "size=0"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args([
"--disk",
format!(
"path={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
)
.as_str(),
cloudinit_params.as_str(),
])
.args(["--net", net_params.as_str()])
.args(["--vsock", format!("cid=3,socket={socket}").as_str()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.capture_output()
.spawn()
.unwrap();
let console_text = String::from("On a branch floating down river a cricket, singing.");
// Create the snapshot directory
let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir);
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check the number of vCPUs
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4);
// Check the guest RAM
let total_memory = guest.get_total_memory().unwrap_or_default();
if use_hotplug {
assert!(total_memory > 1_900_000, "total memory: {total_memory}");
} else {
assert!(total_memory > 900_000, "total memory: {total_memory}");
}
if use_hotplug {
// Increase guest RAM with virtio-mem
resize_command(
&api_socket_source,
None,
Some(6 << 30),
None,
Some(&event_path),
);
thread::sleep(std::time::Duration::new(5, 0));
assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000);
// Use balloon to remove RAM from the VM
resize_command(
&api_socket_source,
None,
None,
Some(1 << 30),
Some(&event_path),
);
thread::sleep(std::time::Duration::new(5, 0));
let total_memory = guest.get_total_memory().unwrap_or_default();
assert!(total_memory > 4_800_000, "total_memory is {total_memory}");
assert!(total_memory < 5_760_000, "total_memory is {total_memory}");
}
// Check the guest virtio-devices, e.g. block, rng, vsock, console, and net
guest.check_devices_common(Some(&socket), Some(&console_text), None);
// x86_64: We check that removing and adding back the virtio-net device
// does not break the snapshot/restore support for virtio-pci.
// This is an important thing to test as the hotplug will
// trigger a PCI BAR reprogramming, which is a good way of
// checking if the stored resources are correctly restored.
// Unplug the virtio-net device
// AArch64: Device hotplug is currently not supported, skipping here.
#[cfg(target_arch = "x86_64")]
{
assert!(remote_command(
&api_socket_source,
"remove-device",
Some(net_id),
));
thread::sleep(std::time::Duration::new(10, 0));
let latest_events = [&MetaEvent {
event: "device-removed".to_string(),
device_id: Some(net_id.to_string()),
}];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path)
}));
// Plug the virtio-net device again
assert!(remote_command(
&api_socket_source,
"add-net",
Some(net_params.as_str()),
));
thread::sleep(std::time::Duration::new(10, 0));
}
snapshot_restore_common::snapshot_and_check_events(
&api_socket_source,
&snapshot_dir,
&event_path,
);
});
// Shutdown the source VM and check console output
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(&console_text));
});
handle_child_output(r, &output);
// Remove the vsock socket file.
Command::new("rm")
.arg("-f")
.arg(socket.as_str())
.output()
.unwrap();
let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir));
let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir));
// Restore the VM from the snapshot
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_restored])
.args([
"--event-monitor",
format!("path={event_path_restored}").as_str(),
])
.args([
"--restore",
format!("source_url=file://{snapshot_dir},resume={use_resume_option}").as_str(),
])
.capture_output()
.spawn()
.unwrap();
let expected_events = [
&MetaEvent {
event: "starting".to_string(),
device_id: None,
},
&MetaEvent {
event: "activated".to_string(),
device_id: Some("__console".to_string()),
},
&MetaEvent {
event: "activated".to_string(),
device_id: Some("__rng".to_string()),
},
&MetaEvent {
event: "restoring".to_string(),
device_id: None,
},
];
assert!(wait_until(Duration::from_secs(30), || {
check_sequential_events(&expected_events, &event_path_restored)
}));
if use_resume_option {
let latest_events = [
&MetaEvent {
event: "restored".to_string(),
device_id: None,
},
&MetaEvent {
event: "resuming".to_string(),
device_id: None,
},
&MetaEvent {
event: "resumed".to_string(),
device_id: None,
},
];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
} else {
let latest_events = [&MetaEvent {
event: "restored".to_string(),
device_id: None,
}];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
}
// Wait until the restored VM API is ready before issuing follow-up requests.
assert!(wait_until(Duration::from_secs(30), || remote_command(
&api_socket_restored,
"info",
None
)));
// Remove the snapshot dir
let _ = remove_dir_all(snapshot_dir.as_str());
let r = std::panic::catch_unwind(|| {
if use_resume_option {
// VM was automatically resumed via restore option, just wait for events
thread::sleep(std::time::Duration::new(1, 0));
} else {
// Resume the VM manually
assert!(wait_until(Duration::from_secs(30), || remote_command(
&api_socket_restored,
"info",
None
)));
assert!(remote_command(&api_socket_restored, "resume", None));
let latest_events = [
&MetaEvent {
event: "resuming".to_string(),
device_id: None,
},
&MetaEvent {
event: "resumed".to_string(),
device_id: None,
},
];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
}
// Perform same checks to validate VM has been properly restored
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4);
let total_memory = guest.get_total_memory().unwrap_or_default();
if use_hotplug {
assert!(total_memory > 4_800_000, "total_memory is {total_memory}");
assert!(total_memory < 5_760_000, "total_memory is {total_memory}");
// Deflate balloon to restore entire RAM to the VM
resize_command(&api_socket_restored, None, None, Some(0), None);
thread::sleep(std::time::Duration::new(5, 0));
assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000);
// Decrease guest RAM with virtio-mem
resize_command(&api_socket_restored, None, Some(5 << 30), None, None);
thread::sleep(std::time::Duration::new(5, 0));
let total_memory = guest.get_total_memory().unwrap_or_default();
assert!(total_memory > 4_800_000, "total_memory is {total_memory}");
assert!(total_memory < 5_760_000, "total_memory is {total_memory}");
} else {
assert!(total_memory > 900_000, "total memory: {total_memory}");
}
guest.check_devices_common(Some(&socket), Some(&console_text), None);
});
// Shutdown the target VM and check console output
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(&console_text));
});
handle_child_output(r, &output);
}
pub(crate) fn _test_snapshot_restore_uffd(
memory_config: &str,
memory_zone_config: &[&str],
min_total_memory_kib: u32,
) {
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 api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir));
let console_text = String::from("On a branch floating down river a cricket, singing.");
let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir);
let socket = temp_vsock_path(&guest.tmp_dir);
let event_path = temp_event_monitor_path(&guest.tmp_dir);
let mut source_cmd = GuestCommand::new(&guest);
source_cmd
.args(["--api-socket", &api_socket_source])
.args(["--event-monitor", format!("path={event_path}").as_str()])
.args(["--cpus", "boot=4"])
.args(["--memory", memory_config]);
if !memory_zone_config.is_empty() {
source_cmd.args(["--memory-zone"]).args(memory_zone_config);
}
let mut child = source_cmd
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.default_net()
.args(["--vsock", format!("cid=3,socket={socket}").as_str()])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4);
assert!(guest.get_total_memory().unwrap_or_default() > min_total_memory_kib);
guest.check_devices_common(Some(&socket), Some(&console_text), None);
snapshot_and_check_events(&api_socket_source, &snapshot_dir, &event_path);
});
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(&console_text));
});
handle_child_output(r, &output);
Command::new("rm")
.arg("-f")
.arg(socket.as_str())
.output()
.unwrap();
let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir));
let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir));
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_restored])
.args([
"--event-monitor",
format!("path={event_path_restored}").as_str(),
])
.args([
"--restore",
format!("source_url=file://{snapshot_dir},memory_restore_mode=ondemand").as_str(),
])
.capture_output()
.spawn()
.unwrap();
let latest_events = [&MetaEvent {
event: "restored".to_string(),
device_id: None,
}];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
let r = std::panic::catch_unwind(|| {
assert!(wait_until(Duration::from_secs(30), || remote_command(
&api_socket_restored,
"info",
None
)));
assert!(remote_command(&api_socket_restored, "resume", None));
let latest_events = [
&MetaEvent {
event: "resuming".to_string(),
device_id: None,
},
&MetaEvent {
event: "resumed".to_string(),
device_id: None,
},
];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4);
assert!(guest.get_total_memory().unwrap_or_default() > min_total_memory_kib);
guest.check_devices_common(Some(&socket), Some(&console_text), None);
});
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(&console_text));
let logs = format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
logs.contains("UFFD restore: demand-paged restore enabled"),
"Expected UFFD restore path to be enabled. output: {logs}"
);
});
handle_child_output(r, &output);
let _ = remove_dir_all(snapshot_dir.as_str());
}
pub(crate) fn _test_snapshot_restore_devices(pvpanic: 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 api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir));
let device_params = {
let mut data = vec![];
if pvpanic {
data.push(String::from("--pvpanic"));
}
data
};
let socket = temp_vsock_path(&guest.tmp_dir);
let event_path = temp_event_monitor_path(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_source])
.args(["--event-monitor", format!("path={event_path}").as_str()])
.args(["--cpus", "boot=2"])
.args(["--memory", "size=1G"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.default_disks()
.default_net()
.args(["--vsock", format!("cid=3,socket={socket}").as_str()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args(device_params)
.capture_output()
.spawn()
.unwrap();
let console_text = String::from("On a branch floating down river a cricket, singing.");
let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir);
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2);
snapshot_and_check_events(&api_socket_source, &snapshot_dir, &event_path);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
Command::new("rm")
.arg("-f")
.arg(socket.as_str())
.output()
.unwrap();
let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir));
let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir));
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_restored])
.args([
"--event-monitor",
format!("path={event_path_restored}").as_str(),
])
.args([
"--restore",
format!("source_url=file://{snapshot_dir}").as_str(),
])
.capture_output()
.spawn()
.unwrap();
let latest_events = [&MetaEvent {
event: "restored".to_string(),
device_id: None,
}];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
let _ = remove_dir_all(snapshot_dir.as_str());
let r = std::panic::catch_unwind(|| {
assert!(wait_until(Duration::from_secs(30), || remote_command(
&api_socket_restored,
"info",
None
)));
assert!(remote_command(&api_socket_restored, "resume", None));
let latest_events = [
&MetaEvent {
event: "resuming".to_string(),
device_id: None,
},
&MetaEvent {
event: "resumed".to_string(),
device_id: None,
},
];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2);
guest.check_devices_common(Some(&socket), Some(&console_text), None);
if pvpanic {
make_guest_panic(&guest);
thread::sleep(std::time::Duration::new(10, 0));
let expected_sequential_events = [&MetaEvent {
event: "panic".to_string(),
device_id: None,
}];
assert!(check_latest_events_exact(
&expected_sequential_events,
&event_path_restored
));
}
});
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(&console_text));
});
handle_child_output(r, &output);
}
}
mod common_sequential {
#[cfg(not(feature = "mshv"))]
use std::fs::remove_dir_all;
#[cfg(not(feature = "mshv"))]
use crate::*;
#[test]
#[cfg(not(feature = "mshv"))]
fn test_memory_mergeable_on() {
test_memory_mergeable(true);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_snapshot_restore_uffd_hugepage_zone() {
if !exec_host_command_status(
"grep -q '^Hugepagesize:[[:space:]]*2048 kB' /proc/meminfo && test $(awk '/HugePages_Free/ {print $2}' /proc/meminfo) -ge 256",
)
.success()
{
println!("SKIPPED: not enough free 2MiB hugepages for UFFD restore test");
return;
}
snapshot_restore_common::_test_snapshot_restore_uffd(
"size=0",
&["id=mem0,size=512M,hugepages=on,hugepage_size=2M"],
480_000,
);
}
#[test]
#[cfg(not(feature = "mshv"))] // See issue #7437
#[ignore = "See #6970"]
fn test_snapshot_restore_with_fd() {
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 api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir));
let net_id = "net123";
let num_queue_pairs: usize = 2;
// use a name that does not conflict with tap dev created from other tests
let tap_name = "chtap999";
use std::str::FromStr;
let taps = net_util::open_tap(
Some(tap_name),
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 net_params = format!(
"id={},fd=[{},{}],mac={},ip={},mask=255.255.255.128,num_queues={}",
net_id,
taps[0].as_raw_fd(),
taps[1].as_raw_fd(),
guest.network.guest_mac0,
guest.network.host_ip0,
num_queue_pairs * 2
);
let cloudinit_params = format!(
"path={},iommu=on",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
);
let n_cpu = 2;
let event_path = temp_event_monitor_path(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_source])
.args(["--event-monitor", format!("path={event_path}").as_str()])
.args(["--cpus", format!("boot={n_cpu}").as_str()])
.args(["--memory", "size=1G"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args([
"--disk",
format!(
"path={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
)
.as_str(),
cloudinit_params.as_str(),
])
.args(["--net", net_params.as_str()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.capture_output()
.spawn()
.unwrap();
let console_text = String::from("On a branch floating down river a cricket, singing.");
// Create the snapshot directory
let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir);
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// close the fds after VM boots, as CH duplicates them before using
for tap in taps.iter() {
unsafe { libc::close(tap.as_raw_fd()) };
}
// Check the number of vCPUs
assert_eq!(guest.get_cpu_count().unwrap_or_default(), n_cpu);
// Check the guest RAM
assert!(guest.get_total_memory().unwrap_or_default() > 960_000);
// Check the guest virtio-devices, e.g. block, rng, vsock, console, and net
guest.check_devices_common(None, Some(&console_text), None);
snapshot_restore_common::snapshot_and_check_events(
&api_socket_source,
&snapshot_dir,
&event_path,
);
});
// Shutdown the source VM and check console output
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(&console_text));
});
handle_child_output(r, &output);
let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir));
let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir));
// Restore the VM from the snapshot
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_restored])
.args([
"--event-monitor",
format!("path={event_path_restored}").as_str(),
])
.capture_output()
.spawn()
.unwrap();
thread::sleep(std::time::Duration::new(2, 0));
let taps = net_util::open_tap(
Some(tap_name),
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 restore_params = format!(
"source_url=file://{},net_fds=[{}@[{},{}]]",
snapshot_dir,
net_id,
taps[0].as_raw_fd(),
taps[1].as_raw_fd()
);
assert!(remote_command(
&api_socket_restored,
"restore",
Some(restore_params.as_str())
));
// Wait for the VM to be restored
assert!(wait_until(Duration::from_secs(20), || {
remote_command(&api_socket_restored, "info", None)
}));
// close the fds as CH duplicates them before using
for tap in taps.iter() {
unsafe { libc::close(tap.as_raw_fd()) };
}
let expected_events = [
&MetaEvent {
event: "starting".to_string(),
device_id: None,
},
&MetaEvent {
event: "activated".to_string(),
device_id: Some("__console".to_string()),
},
&MetaEvent {
event: "activated".to_string(),
device_id: Some("__rng".to_string()),
},
&MetaEvent {
event: "restoring".to_string(),
device_id: None,
},
];
// Wait for the restore event sequence to be recorded.
assert!(wait_until(Duration::from_secs(30), || {
check_sequential_events(&expected_events, &event_path_restored)
}));
let latest_events = [&MetaEvent {
event: "restored".to_string(),
device_id: None,
}];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
// Remove the snapshot dir
let _ = remove_dir_all(snapshot_dir.as_str());
let r = std::panic::catch_unwind(|| {
// Resume the VM
assert!(wait_until(Duration::from_secs(20), || remote_command(
&api_socket_restored,
"info",
None
)));
assert!(remote_command(&api_socket_restored, "resume", None));
let latest_events = [
&MetaEvent {
event: "resuming".to_string(),
device_id: None,
},
&MetaEvent {
event: "resumed".to_string(),
device_id: None,
},
];
assert!(wait_until(Duration::from_secs(30), || {
check_latest_events_exact(&latest_events, &event_path_restored)
}));
// Perform same checks to validate VM has been properly restored
assert_eq!(guest.get_cpu_count().unwrap_or_default(), n_cpu);
assert!(guest.get_total_memory().unwrap_or_default() > 960_000);
guest.check_devices_common(None, Some(&console_text), None);
});
// Shutdown the target VM and check console output
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(&console_text));
});
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_snapshot_restore_virtio_fs() {
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 api_socket_source = format!("{}.1", temp_api_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");
let (mut daemon_child, virtiofsd_socket_path) =
prepare_virtiofsd(&guest.tmp_dir, shared_dir.to_str().unwrap());
let event_path = temp_event_monitor_path(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_source])
.args(["--event-monitor", format!("path={event_path}").as_str()])
.args(["--cpus", "boot=2"])
.args(["--memory", "size=512M,shared=on"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.default_disks()
.default_net()
.args([
"--fs",
format!("socket={virtiofsd_socket_path},tag=myfs,num_queues=1,queue_size=1024")
.as_str(),
])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.capture_output()
.spawn()
.unwrap();
let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir);
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Mount virtiofs and write a test file
guest
.ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/")
.unwrap();
// Verify the shared directory is accessible
assert_eq!(
guest.ssh_command("cat mount_dir/file1").unwrap().trim(),
"foo"
);
// Write a file from the guest
guest
.ssh_command(
"sudo bash -c 'echo snapshot_test_data > mount_dir/snapshot_test_file'",
)
.unwrap();
snapshot_restore_common::snapshot_and_check_events(
&api_socket_source,
&snapshot_dir,
&event_path,
);
});
// Shutdown the source VM
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
// Kill the old virtiofsd
let _ = daemon_child.kill();
let _ = daemon_child.wait();
// Start a fresh virtiofsd (reusing the same socket path)
let (mut daemon_child, _) = prepare_virtiofsd(&guest.tmp_dir, shared_dir.to_str().unwrap());
let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir));
let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir));
// Restore the VM from the snapshot
let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket_restored])
.args([
"--event-monitor",
format!("path={event_path_restored}").as_str(),
])
.args([
"--restore",
format!("source_url=file://{snapshot_dir}").as_str(),
])
.capture_output()
.spawn()
.unwrap();
// Wait for the VM to be restored
assert!(wait_until(Duration::from_secs(30), || {
remote_command(&api_socket_restored, "info", None)
}));
let latest_events = [&MetaEvent {
event: "restored".to_string(),
device_id: None,
}];
assert!(check_latest_events_exact(
&latest_events,
&event_path_restored
));
// Remove the snapshot dir
let _ = remove_dir_all(snapshot_dir.as_str());
let r = std::panic::catch_unwind(|| {
// Resume the VM
assert!(wait_until(Duration::from_secs(30), || remote_command(
&api_socket_restored,
"info",
None
)));
assert!(remote_command(&api_socket_restored, "resume", None));
thread::sleep(std::time::Duration::new(5, 0));
// Verify virtiofs still works after restore
// Read the file written before snapshot
assert_eq!(
guest
.ssh_command("cat mount_dir/snapshot_test_file")
.unwrap()
.trim(),
"snapshot_test_data"
);
// Read the pre-existing shared file
assert_eq!(
guest.ssh_command("cat mount_dir/file1").unwrap().trim(),
"foo"
);
// Write a new file after restore
guest
.ssh_command("sudo bash -c 'echo post_restore_data > mount_dir/post_restore_file'")
.unwrap();
// Verify the new file exists on the host
let post_restore_content =
std::fs::read_to_string(shared_dir.join("post_restore_file")).unwrap();
assert_eq!(post_restore_content.trim(), "post_restore_data");
});
// Shutdown the target VM
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
// Clean up virtiofsd and test files
let _ = daemon_child.kill();
let _ = daemon_child.wait();
let _ = std::fs::remove_file(shared_dir.join("snapshot_test_file"));
let _ = std::fs::remove_file(shared_dir.join("post_restore_file"));
}
#[cfg(not(feature = "mshv"))]
fn _test_live_migration_balloon(upgrade_test: bool, local: 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 console_text = String::from("On a branch floating down river a cricket, singing.");
let net_id = "net123";
let net_params = format!(
"id={},tap=,mac={},ip={},mask=255.255.255.128",
net_id, guest.network.guest_mac0, guest.network.host_ip0
);
let memory_param: &[&str] = if local {
&[
"--memory",
"size=4G,hotplug_method=virtio-mem,hotplug_size=8G,shared=on",
"--balloon",
"size=0",
]
} else {
&[
"--memory",
"size=4G,hotplug_method=virtio-mem,hotplug_size=8G",
"--balloon",
"size=0",
]
};
let boot_vcpus = 2;
let max_vcpus = 4;
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 pmem_path = String::from("/dev/pmem0");
// Start the source VM
let src_vm_path = if upgrade_test {
cloud_hypervisor_release_path()
} else {
clh_command("cloud-hypervisor")
};
let src_api_socket = temp_api_path(&guest.tmp_dir);
let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path);
src_vm_cmd
.args([
"--cpus",
format!("boot={boot_vcpus},max={max_vcpus}").as_str(),
])
.args(memory_param)
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", net_params.as_str()])
.args(["--api-socket", &src_api_socket])
.args([
"--pmem",
format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(),
]);
let mut src_child = src_vm_cmd.capture_output().spawn().unwrap();
// Start the destination VM
let mut dest_api_socket = temp_api_path(&guest.tmp_dir);
dest_api_socket.push_str(".dest");
let mut dest_child = GuestCommand::new(&guest)
.args(["--api-socket", &dest_api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Make sure the source VM is functional
// Check the number of vCPUs
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
// Check the guest RAM
assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000);
// Increase the guest RAM
resize_command(&src_api_socket, None, Some(6 << 30), None, None);
assert!(wait_until(Duration::from_secs(30), || {
guest.get_total_memory().unwrap_or_default() > 5_760_000
}));
assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000);
// Use balloon to remove RAM from the VM
resize_command(&src_api_socket, None, None, Some(1 << 30), None);
assert!(wait_until(Duration::from_secs(5), || {
let total_memory = guest.get_total_memory().unwrap_or_default();
total_memory > 4_800_000 && total_memory < 5_760_000
}));
let total_memory = guest.get_total_memory().unwrap_or_default();
assert!(total_memory > 4_800_000);
assert!(total_memory < 5_760_000);
// Check the guest virtio-devices, e.g. block, rng, console, and net
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
// x86_64: Following what's done in the `test_snapshot_restore`, we need
// to make sure that removing and adding back the virtio-net device does
// not break the live-migration support for virtio-pci.
#[cfg(target_arch = "x86_64")]
{
assert!(remote_command(
&src_api_socket,
"remove-device",
Some(net_id),
));
assert!(wait_until(Duration::from_secs(10), || {
guest.wait_for_ssh(Duration::from_secs(1)).is_err()
}));
// Plug the virtio-net device again
assert!(remote_command(
&src_api_socket,
"add-net",
Some(net_params.as_str()),
));
guest.wait_for_ssh(Duration::from_secs(10)).unwrap();
}
// Start the live-migration
let migration_socket = String::from(
guest
.tmp_dir
.as_path()
.join("live-migration.sock")
.to_str()
.unwrap(),
);
assert!(
start_live_migration(
&migration_socket,
&src_api_socket,
&dest_api_socket,
local,
false
),
"Unsuccessful command: 'send-migration' or 'receive-migration'."
);
});
// Check and report any errors occurred during the live-migration
if r.is_err() {
print_and_panic(
src_child,
dest_child,
None,
"Error occurred during live-migration",
);
}
// Check the source vm has been terminated successful (give it '3s' to settle)
thread::sleep(std::time::Duration::new(3, 0));
if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) {
print_and_panic(
src_child,
dest_child,
None,
"source VM was not terminated successfully.",
);
}
// Post live-migration check to make sure the destination VM is functional
let r = std::panic::catch_unwind(|| {
// Perform same checks to validate VM has been properly migrated
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000);
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
// Perform checks on guest RAM using balloon
let total_memory = guest.get_total_memory().unwrap_or_default();
assert!(total_memory > 4_800_000);
assert!(total_memory < 5_760_000);
// Deflate balloon to restore entire RAM to the VM
resize_command(&dest_api_socket, None, None, Some(0), None);
thread::sleep(std::time::Duration::new(5, 0));
assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000);
// Decrease guest RAM with virtio-mem
resize_command(&dest_api_socket, None, Some(5 << 30), None, None);
thread::sleep(std::time::Duration::new(5, 0));
let total_memory = guest.get_total_memory().unwrap_or_default();
assert!(total_memory > 4_800_000);
assert!(total_memory < 5_760_000);
});
// Clean-up the destination VM and make sure it terminated correctly
let _ = dest_child.kill();
let dest_output = dest_child.wait_with_output().unwrap();
handle_child_output(r, &dest_output);
// Check the destination VM has the expected 'console_text' from its output
let r = std::panic::catch_unwind(|| {
assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text));
});
handle_child_output(r, &dest_output);
}
#[cfg(not(feature = "mshv"))]
fn _test_live_migration_numa(upgrade_test: bool, local: 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 console_text = String::from("On a branch floating down river a cricket, singing.");
let net_id = "net123";
let net_params = format!(
"id={},tap=,mac={},ip={},mask=255.255.255.128",
net_id, guest.network.guest_mac0, guest.network.host_ip0
);
let memory_param: &[&str] = if local {
&[
"--memory",
"size=0,hotplug_method=virtio-mem,shared=on",
"--memory-zone",
"id=mem0,size=1G,hotplug_size=4G,shared=on",
"id=mem1,size=1G,hotplug_size=4G,shared=on",
"id=mem2,size=2G,hotplug_size=4G,shared=on",
"--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",
]
} else {
&[
"--memory",
"size=0,hotplug_method=virtio-mem",
"--memory-zone",
"id=mem0,size=1G,hotplug_size=4G",
"id=mem1,size=1G,hotplug_size=4G",
"id=mem2,size=2G,hotplug_size=4G",
"--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",
]
};
let boot_vcpus = 6;
let max_vcpus = 12;
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 pmem_path = String::from("/dev/pmem0");
// Start the source VM
let src_vm_path = if upgrade_test {
cloud_hypervisor_release_path()
} else {
clh_command("cloud-hypervisor")
};
let src_api_socket = temp_api_path(&guest.tmp_dir);
let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path);
src_vm_cmd
.args([
"--cpus",
format!("boot={boot_vcpus},max={max_vcpus}").as_str(),
])
.args(memory_param)
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", net_params.as_str()])
.args(["--api-socket", &src_api_socket])
.args([
"--pmem",
format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(),
]);
let mut src_child = src_vm_cmd.capture_output().spawn().unwrap();
// Start the destination VM
let mut dest_api_socket = temp_api_path(&guest.tmp_dir);
dest_api_socket.push_str(".dest");
let mut dest_child = GuestCommand::new(&guest)
.args(["--api-socket", &dest_api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Make sure the source VM is functional
// Check the number of vCPUs
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
// Check the guest RAM
assert!(guest.get_total_memory().unwrap_or_default() > 2_880_000);
// Check the guest virtio-devices, e.g. block, rng, console, and net
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
// Check the NUMA parameters are applied correctly and resize
// each zone to test the case where we migrate a VM with the
// virtio-mem regions being used.
{
guest.check_numa_common(
Some(&[960_000, 960_000, 1_920_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(&src_api_socket, "mem0", "2G");
resize_zone_command(&src_api_socket, "mem1", "2G");
resize_zone_command(&src_api_socket, "mem2", "3G");
thread::sleep(std::time::Duration::new(5, 0));
guest.check_numa_common(Some(&[1_920_000, 1_920_000, 1_920_000]), None, None);
}
}
// x86_64: Following what's done in the `test_snapshot_restore`, we need
// to make sure that removing and adding back the virtio-net device does
// not break the live-migration support for virtio-pci.
#[cfg(target_arch = "x86_64")]
{
assert!(remote_command(
&src_api_socket,
"remove-device",
Some(net_id),
));
assert!(wait_until(Duration::from_secs(10), || {
guest.wait_for_ssh(Duration::from_secs(1)).is_err()
}));
// Plug the virtio-net device again
assert!(remote_command(
&src_api_socket,
"add-net",
Some(net_params.as_str()),
));
guest.wait_for_ssh(Duration::from_secs(10)).unwrap();
}
// Start the live-migration
let migration_socket = String::from(
guest
.tmp_dir
.as_path()
.join("live-migration.sock")
.to_str()
.unwrap(),
);
assert!(
start_live_migration(
&migration_socket,
&src_api_socket,
&dest_api_socket,
local,
false
),
"Unsuccessful command: 'send-migration' or 'receive-migration'."
);
});
// Check and report any errors occurred during the live-migration
if r.is_err() {
print_and_panic(
src_child,
dest_child,
None,
"Error occurred during live-migration",
);
}
// Check the source vm has been terminated successful (give it '3s' to settle)
thread::sleep(std::time::Duration::new(3, 0));
if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) {
print_and_panic(
src_child,
dest_child,
None,
"source VM was not terminated successfully.",
);
}
// Post live-migration check to make sure the destination VM is functional
let r = std::panic::catch_unwind(|| {
// Perform same checks to validate VM has been properly migrated
assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus);
#[cfg(target_arch = "x86_64")]
assert!(guest.get_total_memory().unwrap_or_default() > 6_720_000);
#[cfg(target_arch = "aarch64")]
assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000);
guest.check_devices_common(None, Some(&console_text), Some(&pmem_path));
// Perform NUMA related checks
{
#[cfg(target_arch = "aarch64")]
{
guest.check_numa_common(
Some(&[960_000, 960_000, 1_920_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.check_numa_common(
Some(&[1_920_000, 1_920_000, 2_880_000]),
Some(&[&[0, 1, 2], &[3, 4], &[5]]),
Some(&["10 15 20", "20 10 25", "25 30 10"]),
);
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(&dest_api_socket, "mem0", "4G");
resize_zone_command(&dest_api_socket, "mem1", "4G");
resize_zone_command(&dest_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(&dest_api_socket, Some(max_vcpus), 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,
);
}
}
});
// Clean-up the destination VM and make sure it terminated correctly
let _ = dest_child.kill();
let dest_output = dest_child.wait_with_output().unwrap();
handle_child_output(r, &dest_output);
// Check the destination VM has the expected 'console_text' from its output
let r = std::panic::catch_unwind(|| {
assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text));
});
handle_child_output(r, &dest_output);
}
#[cfg(not(feature = "mshv"))]
fn _test_live_migration_ovs_dpdk(upgrade_test: bool, local: bool) {
let ovs_disk_config = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
let ovs_guest = Guest::new(Box::new(ovs_disk_config));
let migration_disk_config = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
let migration_guest = Guest::new(Box::new(migration_disk_config));
let src_api_socket = temp_api_path(&migration_guest.tmp_dir);
// Start two VMs that are connected through ovs-dpdk and one of the VMs is the source VM for live-migration
let (mut ovs_child, mut src_child) =
setup_ovs_dpdk_guests(&ovs_guest, &migration_guest, &src_api_socket, upgrade_test);
// Start the destination VM
let mut dest_api_socket = temp_api_path(&migration_guest.tmp_dir);
dest_api_socket.push_str(".dest");
let mut dest_child = GuestCommand::new(&migration_guest)
.args(["--api-socket", &dest_api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
// Give it '1s' to make sure the 'dest_api_socket' file is properly created
thread::sleep(std::time::Duration::new(1, 0));
// Start the live-migration
let migration_socket = String::from(
migration_guest
.tmp_dir
.as_path()
.join("live-migration.sock")
.to_str()
.unwrap(),
);
assert!(
start_live_migration(
&migration_socket,
&src_api_socket,
&dest_api_socket,
local,
false
),
"Unsuccessful command: 'send-migration' or 'receive-migration'."
);
});
// Check and report any errors occurred during the live-migration
if r.is_err() {
print_and_panic(
src_child,
dest_child,
Some(ovs_child),
"Error occurred during live-migration",
);
}
// Check the source vm has been terminated successful (give it '3s' to settle)
thread::sleep(std::time::Duration::new(3, 0));
if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) {
print_and_panic(
src_child,
dest_child,
Some(ovs_child),
"source VM was not terminated successfully.",
);
}
// Post live-migration check to make sure the destination VM is functional
let r = std::panic::catch_unwind(|| {
// Perform same checks to validate VM has been properly migrated
// Spawn a new netcat listener in the OVS VM
let guest_ip = ovs_guest.network.guest_ip0.clone();
thread::spawn(move || {
ssh_command_ip(
"nc -l 12345",
&guest_ip,
DEFAULT_SSH_RETRIES,
DEFAULT_SSH_TIMEOUT,
)
.unwrap();
});
// Wait for the server to be listening
thread::sleep(std::time::Duration::new(5, 0));
// And check the connection is still functional after live-migration
migration_guest
.ssh_command("nc -vz 172.100.0.1 12345")
.unwrap();
});
// Clean-up the destination VM and OVS VM, and make sure they terminated correctly
let _ = dest_child.kill();
let _ = ovs_child.kill();
let dest_output = dest_child.wait_with_output().unwrap();
let ovs_output = ovs_child.wait_with_output().unwrap();
cleanup_ovs_dpdk();
handle_child_output(r, &dest_output);
handle_child_output(Ok(()), &ovs_output);
}
// NUMA and balloon live migration tests run sequentially
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_balloon() {
_test_live_migration_balloon(false, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_balloon_local() {
_test_live_migration_balloon(false, true);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_upgrade_balloon() {
_test_live_migration_balloon(true, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_upgrade_balloon_local() {
_test_live_migration_balloon(true, true);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_numa() {
_test_live_migration_numa(false, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_numa_local() {
_test_live_migration_numa(false, true);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_upgrade_numa() {
_test_live_migration_numa(true, false);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_live_upgrade_numa_local() {
_test_live_migration_numa(true, true);
}
// Require to run ovs-dpdk tests sequentially because they rely on the same ovs-dpdk setup
#[test]
#[ignore = "See #5532"]
#[cfg(target_arch = "x86_64")]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_ovs_dpdk() {
_test_live_migration_ovs_dpdk(false, false);
}
#[test]
#[ignore = "See #5532 and #7689"]
#[cfg(target_arch = "x86_64")]
#[cfg(not(feature = "mshv"))]
fn test_live_migration_ovs_dpdk_local() {
_test_live_migration_ovs_dpdk(false, true);
}
#[test]
#[ignore = "See #5532"]
#[cfg(target_arch = "x86_64")]
#[cfg(not(feature = "mshv"))]
fn test_live_upgrade_ovs_dpdk() {
_test_live_migration_ovs_dpdk(true, false);
}
#[test]
#[ignore = "See #5532"]
#[cfg(target_arch = "x86_64")]
#[cfg(not(feature = "mshv"))]
fn test_live_upgrade_ovs_dpdk_local() {
_test_live_migration_ovs_dpdk(true, true);
}
}
mod windows {
use std::sync::LazyLock;
use crate::*;
static NEXT_DISK_ID: LazyLock<Mutex<u8>> = LazyLock::new(|| Mutex::new(1));
struct WindowsGuest {
guest: Guest,
auth: PasswordAuth,
}
trait FsType {
const FS_FAT: u8;
const FS_NTFS: u8;
}
impl FsType for WindowsGuest {
const FS_FAT: u8 = 0;
const FS_NTFS: u8 = 1;
}
impl WindowsGuest {
fn new() -> Self {
let disk = WindowsDiskConfig::new(WINDOWS_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk));
let auth = PasswordAuth {
username: String::from("administrator"),
password: String::from("Admin123"),
};
WindowsGuest { guest, auth }
}
fn guest(&self) -> &Guest {
&self.guest
}
fn ssh_cmd(&self, cmd: &str) -> String {
ssh_command_ip_with_auth_retry(
cmd,
&self.auth,
&self.guest.network.guest_ip0,
DEFAULT_SSH_RETRIES,
DEFAULT_SSH_TIMEOUT,
)
.unwrap()
}
fn cpu_count(&self) -> u8 {
self.ssh_cmd("powershell -Command \"(Get-CimInstance win32_computersystem).NumberOfLogicalProcessors\"")
.trim()
.parse::<u8>()
.unwrap_or(0)
}
fn ram_size(&self) -> usize {
self.ssh_cmd("powershell -Command \"(Get-CimInstance win32_computersystem).TotalPhysicalMemory\"")
.trim()
.parse::<usize>()
.unwrap_or(0)
}
fn netdev_count(&self) -> u8 {
self.ssh_cmd("powershell -Command \"netsh int ipv4 show interfaces | Select-String ethernet | Measure-Object -Line | Format-Table -HideTableHeaders\"")
.trim()
.parse::<u8>()
.unwrap_or(0)
}
fn disk_count(&self) -> u8 {
self.ssh_cmd("powershell -Command \"Get-Disk | Measure-Object -Line | Format-Table -HideTableHeaders\"")
.trim()
.parse::<u8>()
.unwrap_or(0)
}
fn reboot(&self) {
let _ = self.ssh_cmd("shutdown /r /t 0");
}
fn shutdown(&self) {
let _ = self.ssh_cmd("shutdown /s /t 0");
}
fn run_dnsmasq(&self) -> std::process::Child {
let listen_address = format!("--listen-address={}", self.guest.network.host_ip0);
let dhcp_host = format!(
"--dhcp-host={},{}",
self.guest.network.guest_mac0, self.guest.network.guest_ip0
);
let dhcp_range = format!(
"--dhcp-range=eth,{},{}",
self.guest.network.guest_ip0, self.guest.network.guest_ip0
);
Command::new("dnsmasq")
.arg("--no-daemon")
.arg("--log-queries")
.arg(listen_address.as_str())
.arg("--except-interface=lo")
.arg("--bind-dynamic") // Allow listening to host_ip while the interface is not ready yet.
.arg("--conf-file=/dev/null")
.arg(dhcp_host.as_str())
.arg(dhcp_range.as_str())
.spawn()
.unwrap()
}
// TODO Cleanup image file explicitly after test, if there's some space issues.
fn disk_new(&self, fs: u8, sz: usize) -> String {
let mut guard = NEXT_DISK_ID.lock().unwrap();
let id = *guard;
*guard = id + 1;
let img = PathBuf::from(format!("/tmp/test-hotplug-{id}.raw"));
let _ = fs::remove_file(&img);
// Create an image file
let out = Command::new("qemu-img")
.args([
"create",
"-f",
"raw",
img.to_str().unwrap(),
format!("{sz}m").as_str(),
])
.output()
.expect("qemu-img command failed")
.stdout;
println!("{out:?}");
// Associate image to a loop device
let out = Command::new("losetup")
.args(["--show", "-f", img.to_str().unwrap()])
.output()
.expect("failed to create loop device")
.stdout;
let _tmp = String::from_utf8_lossy(&out);
let loop_dev = _tmp.trim();
println!("{out:?}");
// Create a partition table
// echo 'type=7' | sudo sfdisk "${LOOP}"
let mut child = Command::new("sfdisk")
.args([loop_dev])
.stdin(Stdio::piped())
.spawn()
.unwrap();
let stdin = child.stdin.as_mut().expect("failed to open stdin");
stdin
.write_all("type=7".as_bytes())
.expect("failed to write stdin");
let out = child.wait_with_output().expect("sfdisk failed").stdout;
println!("{out:?}");
// Disengage the loop device
let out = Command::new("losetup")
.args(["-d", loop_dev])
.output()
.expect("loop device not found")
.stdout;
println!("{out:?}");
// Re-associate loop device pointing to the partition only
let out = Command::new("losetup")
.args([
"--show",
"--offset",
(512 * 2048).to_string().as_str(),
"-f",
img.to_str().unwrap(),
])
.output()
.expect("failed to create loop device")
.stdout;
let _tmp = String::from_utf8_lossy(&out);
let loop_dev = _tmp.trim();
println!("{out:?}");
// Create filesystem.
let fs_cmd = match fs {
WindowsGuest::FS_FAT => "mkfs.msdos",
WindowsGuest::FS_NTFS => "mkfs.ntfs",
_ => panic!("Unknown filesystem type '{fs}'"),
};
let out = Command::new(fs_cmd)
.args([&loop_dev])
.output()
.unwrap_or_else(|_| panic!("{fs_cmd} failed"))
.stdout;
println!("{out:?}");
// Disengage the loop device
let out = Command::new("losetup")
.args(["-d", loop_dev])
.output()
.unwrap_or_else(|_| panic!("loop device '{loop_dev}' not found"))
.stdout;
println!("{out:?}");
img.to_str().unwrap().to_string()
}
fn disks_set_rw(&self) {
let _ = self.ssh_cmd("powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsReadOnly $False\"");
}
fn disks_online(&self) {
let _ = self.ssh_cmd("powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsOffline $False\"");
}
fn disk_file_put(&self, fname: &str, data: &str) {
let _ = self.ssh_cmd(&format!(
"powershell -Command \"'{data}' | Set-Content -Path {fname}\""
));
}
fn disk_file_read(&self, fname: &str) -> String {
self.ssh_cmd(&format!(
"powershell -Command \"Get-Content -Path {fname}\""
))
}
fn wait_for_boot(&self) -> Result<(), WaitForSshError> {
let out = wait_for_ssh(
"dir /b c:\\ | find \"Windows\"",
&self.auth,
&self.guest.network.guest_ip0,
Duration::from_secs(180),
)?;
if out.trim() == "Windows" {
Ok(())
} else {
panic!("Unexpected Windows boot probe output: {:?}", out.trim());
}
}
}
fn vcpu_threads_count(pid: u32) -> u8 {
// ps -T -p 12345 | grep vcpu | wc -l
let out = Command::new("ps")
.args(["-T", "-p", format!("{pid}").as_str()])
.output()
.expect("ps command failed")
.stdout;
String::from_utf8_lossy(&out).matches("vcpu").count() as u8
}
fn netdev_ctrl_threads_count(pid: u32) -> u8 {
// ps -T -p 12345 | grep "_net[0-9]*_ctrl" | wc -l
let out = Command::new("ps")
.args(["-T", "-p", format!("{pid}").as_str()])
.output()
.expect("ps command failed")
.stdout;
let mut n = 0;
String::from_utf8_lossy(&out)
.split_whitespace()
.for_each(|s| n += (s.starts_with("_net") && s.ends_with("_ctrl")) as u8); // _net1_ctrl
n
}
fn disk_ctrl_threads_count(pid: u32) -> u8 {
// ps -T -p 15782 | grep "_disk[0-9]*_q0" | wc -l
let out = Command::new("ps")
.args(["-T", "-p", format!("{pid}").as_str()])
.output()
.expect("ps command failed")
.stdout;
let mut n = 0;
String::from_utf8_lossy(&out)
.split_whitespace()
.for_each(|s| n += (s.starts_with("_disk") && s.ends_with("_q0")) as u8); // _disk0_q0, don't care about multiple queues as they're related to the same hdd
n
}
#[test]
fn test_windows_guest() {
let windows_guest = WindowsGuest::new();
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--cpus", "boot=2,kvm_hyperv=on"])
.args(["--memory", "size=4G"])
.args(["--kernel", edk2_path().to_str().unwrap()])
.args(["--serial", "tty"])
.args(["--console", "off"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let fd = child.stdout.as_ref().unwrap().as_raw_fd();
let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) };
let fd = child.stderr.as_ref().unwrap().as_raw_fd();
let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) };
assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE);
let mut child_dnsmasq = windows_guest.run_dnsmasq();
let r = std::panic::catch_unwind(|| {
// Wait to make sure Windows boots up
windows_guest.wait_for_boot().unwrap();
windows_guest.shutdown();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(60));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
let _ = child_dnsmasq.kill();
let _ = child_dnsmasq.wait();
handle_child_output(r, &output);
}
#[test]
fn test_windows_guest_multiple_queues() {
let windows_guest = WindowsGuest::new();
let mut ovmf_path = dirs::home_dir().unwrap();
ovmf_path.push("workloads");
ovmf_path.push(OVMF_NAME);
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--cpus", "boot=4,kvm_hyperv=on"])
.args(["--memory", "size=4G"])
.args(["--kernel", ovmf_path.to_str().unwrap()])
.args(["--serial", "tty"])
.args(["--console", "off"])
.args([
"--disk",
format!(
"path={},num_queues=4",
windows_guest
.guest()
.disk_config
.disk(DiskType::OperatingSystem)
.unwrap()
)
.as_str(),
])
.args([
"--net",
format!(
"tap=,mac={},ip={},mask=255.255.255.128,num_queues=8",
windows_guest.guest().network.guest_mac0,
windows_guest.guest().network.host_ip0
)
.as_str(),
])
.capture_output()
.spawn()
.unwrap();
let fd = child.stdout.as_ref().unwrap().as_raw_fd();
let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) };
let fd = child.stderr.as_ref().unwrap().as_raw_fd();
let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) };
assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE);
let mut child_dnsmasq = windows_guest.run_dnsmasq();
let r = std::panic::catch_unwind(|| {
// Wait to make sure Windows boots up
windows_guest.wait_for_boot().unwrap();
windows_guest.shutdown();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(60));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
let _ = child_dnsmasq.kill();
let _ = child_dnsmasq.wait();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))]
#[cfg_attr(target_arch = "aarch64", ignore = "See #4327")]
fn test_windows_guest_snapshot_restore() {
let windows_guest = WindowsGuest::new();
let mut ovmf_path = dirs::home_dir().unwrap();
ovmf_path.push("workloads");
ovmf_path.push(OVMF_NAME);
let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap();
let api_socket_source = format!("{}.1", temp_api_path(&tmp_dir));
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--api-socket", &api_socket_source])
.args(["--cpus", "boot=2,kvm_hyperv=on"])
.args(["--memory", "size=4G"])
.args(["--kernel", ovmf_path.to_str().unwrap()])
.args(["--serial", "tty"])
.args(["--console", "off"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let fd = child.stdout.as_ref().unwrap().as_raw_fd();
let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) };
let fd = child.stderr.as_ref().unwrap().as_raw_fd();
let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) };
assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE);
let mut child_dnsmasq = windows_guest.run_dnsmasq();
// Wait to make sure Windows boots up
windows_guest.wait_for_boot().unwrap();
let snapshot_dir = temp_snapshot_dir_path(&tmp_dir);
// Pause the VM
assert!(remote_command(&api_socket_source, "pause", None));
// Take a snapshot from the VM
assert!(remote_command(
&api_socket_source,
"snapshot",
Some(format!("file://{snapshot_dir}").as_str()),
));
let snapshot_state_path = std::path::Path::new(&snapshot_dir).join("state.json");
let snapshot_config_path = std::path::Path::new(&snapshot_dir).join("config.json");
assert!(wait_until(Duration::from_secs(30), || {
snapshot_state_path.exists() && snapshot_config_path.exists()
}));
let _ = child.kill();
child.wait().unwrap();
let api_socket_restored = format!("{}.2", temp_api_path(&tmp_dir));
// Restore the VM from the snapshot
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--api-socket", &api_socket_restored])
.args([
"--restore",
format!("source_url=file://{snapshot_dir}").as_str(),
])
.capture_output()
.spawn()
.unwrap();
// Wait for the VM to be restored
assert!(wait_until(Duration::from_secs(30), || {
remote_command(&api_socket_restored, "info", None)
}));
let r = std::panic::catch_unwind(|| {
// Resume the VM
assert!(wait_until(Duration::from_secs(30), || remote_command(
&api_socket_restored,
"info",
None
)));
assert!(remote_command(&api_socket_restored, "resume", None));
windows_guest.shutdown();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(60));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
let _ = child_dnsmasq.kill();
let _ = child_dnsmasq.wait();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))]
#[cfg(not(target_arch = "aarch64"))]
fn test_windows_guest_cpu_hotplug() {
let windows_guest = WindowsGuest::new();
let mut ovmf_path = dirs::home_dir().unwrap();
ovmf_path.push("workloads");
ovmf_path.push(OVMF_NAME);
let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap();
let api_socket = temp_api_path(&tmp_dir);
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--api-socket", &api_socket])
.args(["--cpus", "boot=2,max=8,kvm_hyperv=on"])
.args(["--memory", "size=4G"])
.args(["--kernel", ovmf_path.to_str().unwrap()])
.args(["--serial", "tty"])
.args(["--console", "off"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let mut child_dnsmasq = windows_guest.run_dnsmasq();
let r = std::panic::catch_unwind(|| {
// Wait to make sure Windows boots up
windows_guest.wait_for_boot().unwrap();
let vcpu_num = 2;
// Check the initial number of CPUs the guest sees
assert_eq!(windows_guest.cpu_count(), vcpu_num);
// Check the initial number of vcpu threads in the CH process
assert_eq!(vcpu_threads_count(child.id()), vcpu_num);
let vcpu_num = 6;
// Hotplug some CPUs
resize_command(&api_socket, Some(vcpu_num), None, None, None);
// Wait for Windows to report the hotplugged CPUs.
assert!(wait_until(Duration::from_secs(10), || windows_guest
.cpu_count()
== vcpu_num));
// Check the guest sees the correct number
assert_eq!(windows_guest.cpu_count(), vcpu_num);
// Check the CH process has the correct number of vcpu threads
assert_eq!(vcpu_threads_count(child.id()), vcpu_num);
let vcpu_num = 4;
// Remove some CPUs. Note that Windows doesn't support hot-remove.
resize_command(&api_socket, Some(vcpu_num), None, None, None);
thread::sleep(std::time::Duration::new(10, 0));
// Reboot to let Windows catch up
windows_guest.reboot();
// Wait for Windows to come back after the reboot.
windows_guest.wait_for_boot().unwrap();
// Wait for Windows to reflect the unplugged CPU count.
assert!(wait_until(Duration::from_secs(60), || windows_guest
.cpu_count()
== vcpu_num));
// Check the guest sees the correct number
assert_eq!(windows_guest.cpu_count(), vcpu_num);
// Check the CH process has the correct number of vcpu threads
assert_eq!(vcpu_threads_count(child.id()), vcpu_num);
windows_guest.shutdown();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(60));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
let _ = child_dnsmasq.kill();
let _ = child_dnsmasq.wait();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))]
#[cfg(not(target_arch = "aarch64"))]
fn test_windows_guest_ram_hotplug() {
let windows_guest = WindowsGuest::new();
let mut ovmf_path = dirs::home_dir().unwrap();
ovmf_path.push("workloads");
ovmf_path.push(OVMF_NAME);
let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap();
let api_socket = temp_api_path(&tmp_dir);
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--api-socket", &api_socket])
.args(["--cpus", "boot=2,kvm_hyperv=on"])
.args(["--memory", "size=2G,hotplug_size=5G"])
.args(["--kernel", ovmf_path.to_str().unwrap()])
.args(["--serial", "tty"])
.args(["--console", "off"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let mut child_dnsmasq = windows_guest.run_dnsmasq();
let r = std::panic::catch_unwind(|| {
// Wait to make sure Windows boots up
windows_guest.wait_for_boot().unwrap();
let ram_size = 2 * 1024 * 1024 * 1024;
// Check the initial number of RAM the guest sees
let current_ram_size = windows_guest.ram_size();
// This size seems to be reserved by the system and thus the
// reported amount differs by this constant value.
let reserved_ram_size = ram_size - current_ram_size;
// Verify that there's not more than 4mb constant diff wasted
// by the reserved ram.
assert!(reserved_ram_size < 4 * 1024 * 1024);
let ram_size = 4 * 1024 * 1024 * 1024;
// Hotplug some RAM
resize_command(&api_socket, None, Some(ram_size), None, None);
// Wait for Windows to report the hotplugged memory.
assert!(wait_until(Duration::from_secs(10), || windows_guest
.ram_size()
== ram_size - reserved_ram_size));
let ram_size = 3 * 1024 * 1024 * 1024;
// Unplug some RAM. Note that hot-remove most likely won't work.
resize_command(&api_socket, None, Some(ram_size), None, None);
// Reboot to let Windows catch up
windows_guest.reboot();
// Wait for Windows to come back after the reboot.
windows_guest.wait_for_boot().unwrap();
// Wait for Windows to reflect the unplugged RAM amount.
assert!(wait_until(Duration::from_secs(60), || windows_guest
.ram_size()
== ram_size - reserved_ram_size));
// Check the guest sees the correct number
assert_eq!(windows_guest.ram_size(), ram_size - reserved_ram_size);
windows_guest.shutdown();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(60));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
let _ = child_dnsmasq.kill();
let _ = child_dnsmasq.wait();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))]
fn test_windows_guest_netdev_hotplug() {
let windows_guest = WindowsGuest::new();
let mut ovmf_path = dirs::home_dir().unwrap();
ovmf_path.push("workloads");
ovmf_path.push(OVMF_NAME);
let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap();
let api_socket = temp_api_path(&tmp_dir);
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--api-socket", &api_socket])
.args(["--cpus", "boot=2,kvm_hyperv=on"])
.args(["--memory", "size=4G"])
.args(["--kernel", ovmf_path.to_str().unwrap()])
.args(["--serial", "tty"])
.args(["--console", "off"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let mut child_dnsmasq = windows_guest.run_dnsmasq();
let r = std::panic::catch_unwind(|| {
// Wait to make sure Windows boots up
windows_guest.wait_for_boot().unwrap();
// Initially present network device
let netdev_num = 1;
assert_eq!(windows_guest.netdev_count(), netdev_num);
assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num);
// Hotplug network device
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-net",
Some(windows_guest.guest().default_net_string().as_str()),
);
assert!(cmd_success);
assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"_net2\""));
// Wait for Windows to enumerate the added network device.
assert!(wait_until(Duration::from_secs(5), || windows_guest
.netdev_count()
== 2
&& netdev_ctrl_threads_count(child.id()) == 2));
// Verify the device is on the system
let netdev_num = 2;
assert_eq!(windows_guest.netdev_count(), netdev_num);
assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num);
// Remove network device
let cmd_success = remote_command(&api_socket, "remove-device", Some("_net2"));
assert!(cmd_success);
// Wait for Windows to drop the removed network device.
assert!(wait_until(Duration::from_secs(5), || windows_guest
.netdev_count()
== 1
&& netdev_ctrl_threads_count(child.id()) == 1));
// Verify the device has been removed
let netdev_num = 1;
assert_eq!(windows_guest.netdev_count(), netdev_num);
assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num);
windows_guest.shutdown();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(60));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
let _ = child_dnsmasq.kill();
let _ = child_dnsmasq.wait();
handle_child_output(r, &output);
}
#[test]
#[ignore = "See #6037"]
#[cfg(not(feature = "mshv"))]
#[cfg(not(target_arch = "aarch64"))]
fn test_windows_guest_disk_hotplug() {
let windows_guest = WindowsGuest::new();
let mut ovmf_path = dirs::home_dir().unwrap();
ovmf_path.push("workloads");
ovmf_path.push(OVMF_NAME);
let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap();
let api_socket = temp_api_path(&tmp_dir);
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--api-socket", &api_socket])
.args(["--cpus", "boot=2,kvm_hyperv=on"])
.args(["--memory", "size=4G"])
.args(["--kernel", ovmf_path.to_str().unwrap()])
.args(["--serial", "tty"])
.args(["--console", "off"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let mut child_dnsmasq = windows_guest.run_dnsmasq();
let disk = windows_guest.disk_new(WindowsGuest::FS_FAT, 100);
let r = std::panic::catch_unwind(|| {
// Wait to make sure Windows boots up
windows_guest.wait_for_boot().unwrap();
// Initially present disk device
let disk_num = 1;
assert_eq!(windows_guest.disk_count(), disk_num);
assert_eq!(disk_ctrl_threads_count(child.id()), disk_num);
// Hotplug disk device
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-disk",
Some(format!("path={disk},readonly=off").as_str()),
);
assert!(cmd_success);
assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"_disk2\""));
// Online disk device
windows_guest.disks_set_rw();
windows_guest.disks_online();
// Wait for Windows to enumerate the added disk.
assert!(wait_until(Duration::from_secs(5), || windows_guest
.disk_count()
== 2
&& disk_ctrl_threads_count(child.id()) == 2));
// Verify the device is on the system
let disk_num = 2;
assert_eq!(windows_guest.disk_count(), disk_num);
assert_eq!(disk_ctrl_threads_count(child.id()), disk_num);
let data = "hello";
let fname = "d:\\world";
windows_guest.disk_file_put(fname, data);
// Unmount disk device
let cmd_success = remote_command(&api_socket, "remove-device", Some("_disk2"));
assert!(cmd_success);
// Wait for Windows to drop the removed disk.
assert!(wait_until(Duration::from_secs(5), || windows_guest
.disk_count()
== 1
&& disk_ctrl_threads_count(child.id()) == 1));
// Verify the device has been removed
let disk_num = 1;
assert_eq!(windows_guest.disk_count(), disk_num);
assert_eq!(disk_ctrl_threads_count(child.id()), disk_num);
// Remount and check the file exists with the expected contents
let (cmd_success, _cmd_output, _) = remote_command_w_output(
&api_socket,
"add-disk",
Some(format!("path={disk},readonly=off").as_str()),
);
assert!(cmd_success);
// Wait for Windows to mount the re-added disk again.
assert!(wait_until(Duration::from_secs(5), || windows_guest
.disk_file_read(fname)
.trim()
== data));
let out = windows_guest.disk_file_read(fname);
assert_eq!(data, out.trim());
// Intentionally no unmount, it'll happen at shutdown.
windows_guest.shutdown();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(60));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
let _ = child_dnsmasq.kill();
let _ = child_dnsmasq.wait();
handle_child_output(r, &output);
}
#[test]
#[ignore = "See #6037"]
#[cfg(not(feature = "mshv"))]
#[cfg(not(target_arch = "aarch64"))]
fn test_windows_guest_disk_hotplug_multi() {
let windows_guest = WindowsGuest::new();
let mut ovmf_path = dirs::home_dir().unwrap();
ovmf_path.push("workloads");
ovmf_path.push(OVMF_NAME);
let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap();
let api_socket = temp_api_path(&tmp_dir);
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--api-socket", &api_socket])
.args(["--cpus", "boot=2,kvm_hyperv=on"])
.args(["--memory", "size=2G"])
.args(["--kernel", ovmf_path.to_str().unwrap()])
.args(["--serial", "tty"])
.args(["--console", "off"])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let mut child_dnsmasq = windows_guest.run_dnsmasq();
// Predefined data to used at various test stages
let disk_test_data: [[String; 4]; 2] = [
[
"_disk2".to_string(),
windows_guest.disk_new(WindowsGuest::FS_FAT, 123),
"d:\\world".to_string(),
"hello".to_string(),
],
[
"_disk3".to_string(),
windows_guest.disk_new(WindowsGuest::FS_NTFS, 333),
"e:\\hello".to_string(),
"world".to_string(),
],
];
let r = std::panic::catch_unwind(|| {
// Wait to make sure Windows boots up
windows_guest.wait_for_boot().unwrap();
// Initially present disk device
let disk_num = 1;
assert_eq!(windows_guest.disk_count(), disk_num);
assert_eq!(disk_ctrl_threads_count(child.id()), disk_num);
for it in &disk_test_data {
let disk_id = it[0].as_str();
let disk = it[1].as_str();
let expected_disk_num = windows_guest.disk_count() + 1;
let expected_ctrl_threads = disk_ctrl_threads_count(child.id()) + 1;
// Hotplug disk device
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-disk",
Some(format!("path={disk},readonly=off").as_str()),
);
assert!(cmd_success);
assert!(
String::from_utf8_lossy(&cmd_output)
.contains(format!("\"id\":\"{disk_id}\"").as_str())
);
// Wait for disk to appear
assert!(wait_until(Duration::from_secs(5), || {
windows_guest.disk_count() == expected_disk_num
&& disk_ctrl_threads_count(child.id()) == expected_ctrl_threads
}));
// Online disk devices
windows_guest.disks_set_rw();
windows_guest.disks_online();
}
// Verify the devices are on the system
let disk_num = (disk_test_data.len() + 1) as u8;
assert_eq!(windows_guest.disk_count(), disk_num);
assert_eq!(disk_ctrl_threads_count(child.id()), disk_num);
// Put test data
for it in &disk_test_data {
let fname = it[2].as_str();
let data = it[3].as_str();
windows_guest.disk_file_put(fname, data);
}
// Unmount disk devices
for it in &disk_test_data {
let disk_id = it[0].as_str();
let cmd_success = remote_command(&api_socket, "remove-device", Some(disk_id));
assert!(cmd_success);
}
// Wait for Windows to drop all removed disks.
assert!(wait_until(Duration::from_secs(5), || windows_guest
.disk_count()
== 1
&& disk_ctrl_threads_count(child.id()) == 1));
// Verify the devices have been removed
let disk_num = 1;
assert_eq!(windows_guest.disk_count(), disk_num);
assert_eq!(disk_ctrl_threads_count(child.id()), disk_num);
// Remount
for it in &disk_test_data {
let disk = it[1].as_str();
let (cmd_success, _cmd_output, _) = remote_command_w_output(
&api_socket,
"add-disk",
Some(format!("path={disk},readonly=off").as_str()),
);
assert!(cmd_success);
}
// Wait for Windows to enumerate the re-added disks.
assert!(wait_until(Duration::from_secs(5), || {
windows_guest.disk_count() == 4 && disk_ctrl_threads_count(child.id()) == 4
}));
// Check the files exists with the expected contents
for it in &disk_test_data {
let fname = it[2].as_str();
let data = it[3].as_str();
let out = windows_guest.disk_file_read(fname);
assert_eq!(data, out.trim());
}
// Intentionally no unmount, it'll happen at shutdown.
windows_guest.shutdown();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(60));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
let _ = child_dnsmasq.kill();
let _ = child_dnsmasq.wait();
handle_child_output(r, &output);
}
#[test]
#[cfg(not(feature = "mshv"))]
#[cfg(not(target_arch = "aarch64"))]
fn test_windows_guest_netdev_multi() {
let windows_guest = WindowsGuest::new();
let mut ovmf_path = dirs::home_dir().unwrap();
ovmf_path.push("workloads");
ovmf_path.push(OVMF_NAME);
let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap();
let api_socket = temp_api_path(&tmp_dir);
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--api-socket", &api_socket])
.args(["--cpus", "boot=2,kvm_hyperv=on"])
.args(["--memory", "size=4G"])
.args(["--kernel", ovmf_path.to_str().unwrap()])
.args(["--serial", "tty"])
.args(["--console", "off"])
.default_disks()
// The multi net dev config is borrowed from test_multiple_network_interfaces
.args([
"--net",
windows_guest.guest().default_net_string().as_str(),
"tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.0",
"tap=mytap42,mac=fe:1f:9e:e1:60:f2,ip=192.168.4.1,mask=255.255.255.0",
])
.capture_output()
.spawn()
.unwrap();
let mut child_dnsmasq = windows_guest.run_dnsmasq();
let r = std::panic::catch_unwind(|| {
// Wait to make sure Windows boots up
windows_guest.wait_for_boot().unwrap();
let netdev_num = 3;
assert_eq!(windows_guest.netdev_count(), netdev_num);
assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num);
let tap_count = exec_host_command_output("ip link | grep -c mytap42");
assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1");
windows_guest.shutdown();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(60));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
let _ = child_dnsmasq.kill();
let _ = child_dnsmasq.wait();
handle_child_output(r, &output);
}
#[test]
fn test_windows_guest_qcow2_backing_direct() {
let windows_guest = WindowsGuest::new();
let qcow2_path = windows_guest.guest().disk_config.qcow2_disk().unwrap();
let mut child = GuestCommand::new(windows_guest.guest())
.args(["--cpus", "boot=2,kvm_hyperv=on"])
.args(["--memory", "size=4G"])
.args(["--kernel", edk2_path().to_str().unwrap()])
.args(["--serial", "tty"])
.args(["--console", "off"])
.args([
"--disk",
format!("path={qcow2_path},image_type=qcow2,backing_files=on,direct=on").as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let fd = child.stdout.as_ref().unwrap().as_raw_fd();
let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) };
let fd = child.stderr.as_ref().unwrap().as_raw_fd();
let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) };
assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE);
let mut child_dnsmasq = windows_guest.run_dnsmasq();
let r = std::panic::catch_unwind(|| {
windows_guest.wait_for_boot().unwrap();
// Write and read back files through qcow2 + direct I/O.
for i in 0..5 {
let fname = format!("c:\\test-dio-{i}.bin");
let fname2 = format!("c:\\test-dio-{i}-copy.bin");
let size = (i + 1) * 4 * 1024 * 1024;
windows_guest.ssh_cmd(&format!(
"powershell -Command \"\
$r = New-Object byte[] {size}; \
(New-Object Random {i}).NextBytes($r); \
[IO.File]::WriteAllBytes('{fname}', $r)\""
));
let hash_write = windows_guest.ssh_cmd(&format!(
"powershell -Command \"(Get-FileHash '{fname}' -Algorithm SHA256).Hash\""
));
windows_guest.ssh_cmd(&format!("copy {fname} {fname2}"));
let hash_read = windows_guest.ssh_cmd(&format!(
"powershell -Command \"(Get-FileHash '{fname2}' -Algorithm SHA256).Hash\""
));
assert_eq!(hash_write.trim(), hash_read.trim());
}
windows_guest.shutdown();
});
let _ = child.wait_timeout(std::time::Duration::from_secs(60));
let _ = child.kill();
let output = child.wait_with_output().unwrap();
let _ = child_dnsmasq.kill();
let _ = child_dnsmasq.wait();
handle_child_output(r, &output);
}
}
#[cfg(target_arch = "x86_64")]
mod vfio {
use crate::*;
const NVIDIA_VFIO_DEVICE: &str = "/sys/bus/pci/devices/0002:00:01.0";
fn platform_cfg(iommufd: bool) -> String {
if iommufd {
"iommufd=on,vfio_p2p_dma=off".to_string()
} else {
"iommufd=off".to_string()
}
}
fn test_nvidia_card_memory_hotplug(hotplug_method: &str, iommufd: bool) {
let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let api_socket = temp_api_path(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=4"])
.args([
"--memory",
format!("size=4G,hotplug_size=4G,hotplug_method={hotplug_method}").as_str(),
])
.args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()])
.args(["--platform", &platform_cfg(iommufd)])
.args(["--device", format!("path={NVIDIA_VFIO_DEVICE}").as_str()])
.args(["--api-socket", &api_socket])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000);
// Verify the VFIO device works before memory hotplug
guest.check_nvidia_gpu();
guest.enable_memory_hotplug();
// Add RAM to the VM
let desired_ram = 6 << 30;
resize_command(&api_socket, None, Some(desired_ram), None, None);
assert!(wait_until(Duration::from_secs(5), || {
guest.get_total_memory().unwrap_or_default() > 5_760_000
}));
assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000);
// Check the VFIO device works when RAM is increased to 6GiB.
// After guest memory hotplug, the VMM must refresh VFIO/iommufd DMA
// mappings for the passthrough GPU.
assert!(wait_until(Duration::from_secs(10), || guest.check_nvidia_gpu()));
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_nvidia_card_memory_hotplug_acpi() {
test_nvidia_card_memory_hotplug("acpi", false);
}
#[test]
fn test_nvidia_card_memory_hotplug_virtio_mem() {
test_nvidia_card_memory_hotplug("virtio-mem", false);
}
#[test]
fn test_iommufd_nvidia_card_memory_hotplug_acpi() {
test_nvidia_card_memory_hotplug("acpi", true);
}
#[test]
fn test_iommufd_nvidia_card_memory_hotplug_virtio_mem() {
test_nvidia_card_memory_hotplug("virtio-mem", true);
}
fn test_nvidia_card_pci_hotplug_common(iommufd: bool) {
let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let api_socket = temp_api_path(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=4"])
.args(["--memory", "size=1G"])
.args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()])
.args(["--platform", &platform_cfg(iommufd)])
.args(["--api-socket", &api_socket])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Hotplug the card to the VM
let (cmd_success, cmd_output, _) = remote_command_w_output(
&api_socket,
"add-device",
Some(format!("id=vfio0,path={NVIDIA_VFIO_DEVICE}").as_str()),
);
assert!(cmd_success);
assert!(
String::from_utf8_lossy(&cmd_output)
.contains("{\"id\":\"vfio0\",\"bdf\":\"0000:00:06.0\"}")
);
// Check the VFIO device works after hotplug
assert!(wait_until(Duration::from_secs(10), || guest.check_nvidia_gpu()));
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_nvidia_card_pci_hotplug() {
test_nvidia_card_pci_hotplug_common(false);
}
#[test]
fn test_iommufd_nvidia_card_pci_hotplug() {
test_nvidia_card_pci_hotplug_common(true);
}
fn test_nvidia_card_reboot_common(iommufd: bool) {
let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let api_socket = temp_api_path(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=4"])
.args(["--memory", "size=1G"])
.args(["--platform", &platform_cfg(iommufd)])
.args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()])
.args([
"--device",
format!("path={NVIDIA_VFIO_DEVICE},iommu=on").as_str(),
])
.args(["--api-socket", &api_socket])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Check the VFIO device works after boot
assert!(guest.check_nvidia_gpu());
guest.reboot_linux(0);
// Check the VFIO device works after reboot
assert!(guest.check_nvidia_gpu());
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_nvidia_card_reboot() {
test_nvidia_card_reboot_common(false);
}
#[test]
fn test_iommufd_nvidia_card_reboot() {
test_nvidia_card_reboot_common(true);
}
fn test_nvidia_card_iommu_address_width_common(iommufd: bool) {
let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let api_socket = temp_api_path(&guest.tmp_dir);
let platform = format!(
"num_pci_segments=2,iommu_segments=1,iommu_address_width=42,{}",
platform_cfg(iommufd)
);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=4"])
.args(["--memory", "size=1G"])
.args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()])
.args(["--device", format!("path={NVIDIA_VFIO_DEVICE}").as_str()])
.args(["--platform", &platform])
.args(["--api-socket", &api_socket])
.default_disks()
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
assert!(
guest
.ssh_command("sudo dmesg")
.unwrap()
.contains("input address: 42 bits")
);
// Check the VFIO device works after boot
guest.check_nvidia_gpu();
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_nvidia_card_iommu_address_width() {
test_nvidia_card_iommu_address_width_common(false);
}
#[test]
fn test_iommufd_nvidia_card_iommu_address_width() {
test_nvidia_card_iommu_address_width_common(true);
}
fn test_nvidia_guest_numa_generic_initiator_common(iommufd: bool) {
// Skip test if VFIO device is not available or not ready
if !std::path::Path::new(NVIDIA_VFIO_DEVICE).exists() {
println!("SKIPPED: VFIO device {NVIDIA_VFIO_DEVICE} not found");
return;
}
// Check if device is bound to vfio-pci driver
let driver_path = format!("{NVIDIA_VFIO_DEVICE}/driver");
if let Ok(driver) = std::fs::read_link(&driver_path) {
let driver_name = driver.file_name().unwrap_or_default().to_string_lossy();
if driver_name != "vfio-pci" {
println!(
"SKIPPED: VFIO device {NVIDIA_VFIO_DEVICE} bound to {driver_name}, not vfio-pci"
);
return;
}
} else {
println!("SKIPPED: VFIO device {NVIDIA_VFIO_DEVICE} not bound to any driver");
return;
}
let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let api_socket = temp_api_path(&guest.tmp_dir);
// x86_64: Direct kernel boot
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=4"])
.args(["--memory", "size=0"])
.args(["--memory-zone", "id=mem0,size=1G", "id=mem1,size=1G"])
.args([
"--numa",
"guest_numa_id=0,cpus=[0-1],distances=[1@20,2@25],memory_zones=mem0",
"guest_numa_id=1,cpus=[2-3],distances=[0@20,2@30],memory_zones=mem1",
"guest_numa_id=2,device_id=vfio0,distances=[0@25,1@30]",
])
.args(["--platform", &platform_cfg(iommufd)])
.args([
"--device",
&format!("id=vfio0,path={NVIDIA_VFIO_DEVICE},iommu=on"),
])
.args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()])
.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();
// Verify NUMA topology is correct
guest.check_numa_common(
Some(&[960_000, 960_000]),
Some(&[&[0, 1], &[2, 3]]),
Some(&["10 20 25", "20 10 30", "25 30 10"]),
);
// Verify Generic Initiator support is present
// Linux kernel sets has_generic_initiator when it parses Type 5 SRAT entries
let has_gi = guest
.ssh_command(
"cat /sys/devices/system/node/has_generic_initiator 2>/dev/null || echo 0",
)
.unwrap()
.trim()
.to_string();
assert_eq!(
has_gi, "2",
"Generic Initiator support should be detected by kernel"
);
// Verify SRAT table contains Generic Initiator entry (Type 5)
// We'll check that /sys/firmware/acpi/tables/SRAT exists and contains our entry
let srat_check = guest
.ssh_command(
"[ -f /sys/firmware/acpi/tables/SRAT ] && echo 'exists' || echo 'missing'",
)
.unwrap()
.trim()
.to_string();
assert_eq!(
srat_check, "exists",
"SRAT table should exist in guest firmware"
);
// Use hexdump to verify Type 5 entry is present
// Type 5 (0x05) should appear in the SRAT table
let srat_has_type5 = guest
.ssh_command("sudo hexdump -C /sys/firmware/acpi/tables/SRAT | grep -q '05 20' && echo 'found' || echo 'not_found'")
.unwrap()
.trim()
.to_string();
assert_eq!(
srat_has_type5, "found",
"SRAT table should contain Generic Initiator Affinity Structure (Type 5, Length 0x20/32)"
);
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_nvidia_guest_numa_generic_initiator() {
test_nvidia_guest_numa_generic_initiator_common(false);
}
#[test]
fn test_iommufd_nvidia_guest_numa_generic_initiator() {
test_nvidia_guest_numa_generic_initiator_common(true);
}
}
#[cfg(target_arch = "aarch64")]
mod aarch64_acpi {
use crate::*;
#[test]
fn test_simple_launch_acpi() {
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
vec![Box::new(focal)].drain(..).for_each(|disk_config| {
let guest = Guest::new(disk_config);
let mut child = GuestCommand::new(&guest)
.default_cpus()
.default_memory()
.args(["--kernel", edk2_path().to_str().unwrap()])
.default_disks()
.default_net()
.args(["--serial", "tty", "--console", "off"])
.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() > 400_000);
assert_eq!(guest.get_pci_bridge_class().unwrap_or_default(), "0x060000");
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
});
}
#[test]
fn test_guest_numa_nodes_acpi() {
_test_guest_numa_nodes(true);
}
#[test]
fn test_cpu_topology_421_acpi() {
test_cpu_topology(4, 2, 1, true);
}
#[test]
fn test_cpu_topology_142_acpi() {
test_cpu_topology(1, 4, 2, true);
}
#[test]
fn test_cpu_topology_262_acpi() {
test_cpu_topology(2, 6, 2, true);
}
#[test]
fn test_power_button_acpi() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = GuestFactory::new_regular_guest_factory()
.create_guest(Box::new(disk_config))
.with_kernel_path(edk2_path().to_str().unwrap());
_test_power_button(&guest);
}
#[test]
fn test_virtio_iommu() {
_test_virtio_iommu(true);
}
}
mod rate_limiter {
use super::*;
const NET_RATE_LIMITER_RUNTIME: u32 = 20;
const BLOCK_RATE_LIMITER_RUNTIME: u32 = 20;
const BLOCK_RATE_LIMITER_RAMP_TIME: u32 = 5;
// Check if the 'measured' rate is within the expected 'difference' (in percentage)
// compared to given 'limit' rate.
fn check_rate_limit(measured: f64, limit: f64, difference: f64) -> bool {
let upper_limit = limit * (1_f64 + difference);
let lower_limit = limit * (1_f64 - difference);
if measured > lower_limit && measured < upper_limit {
return true;
}
eprintln!(
"\n\n==== Start 'check_rate_limit' failed ==== \
\n\nmeasured={measured}, , lower_limit={lower_limit}, upper_limit={upper_limit} \
\n\n==== End 'check_rate_limit' failed ====\n\n"
);
false
}
fn _test_rate_limiter_net(rx: bool) {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let num_queues = 2;
let queue_size = 256;
let bw_size = 104857600_u64; // bytes
let bw_refill_time = 1000; // ms
let limit_bps = (bw_size * 8 * 1000) as f64 / bw_refill_time as f64;
let net_params = format!(
"tap=,mac={},ip={},mask=255.255.255.128,num_queues={},queue_size={},bw_size={},bw_one_time_burst=0,bw_refill_time={}",
guest.network.guest_mac0,
guest.network.host_ip0,
num_queues,
queue_size,
bw_size,
bw_refill_time,
);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", &format!("boot={}", num_queues / 2)])
.args(["--memory", "size=1G"])
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.default_disks()
.args(["--net", net_params.as_str()])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
let measured_bps = measure_virtio_net_throughput(
NET_RATE_LIMITER_RUNTIME,
num_queues / 2,
&guest,
rx,
true,
)
.unwrap();
assert!(check_rate_limit(measured_bps, limit_bps, 0.1));
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_rate_limiter_net_rx() {
_test_rate_limiter_net(true);
}
#[test]
fn test_rate_limiter_net_tx() {
_test_rate_limiter_net(false);
}
fn _test_rate_limiter_block(bandwidth: bool, num_queues: u32) {
let fio_ops = FioOps::RandRW;
let bw_size = if bandwidth {
104857600_u64 // bytes
} else {
1000_u64 // I/O
};
let bw_refill_time = 1000; // ms
let limit_rate = (bw_size * 1000) as f64 / bw_refill_time as f64;
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 test_img_dir = TempDir::new_with_prefix("/var/tmp/ch").unwrap();
let blk_rate_limiter_test_img =
String::from(test_img_dir.as_path().join("blk.img").to_str().unwrap());
// Create the test block image
assert!(
exec_host_command_output(&format!(
"dd if=/dev/zero of={blk_rate_limiter_test_img} bs=1M count=1024"
))
.status
.success()
);
let test_blk_params = if bandwidth {
format!(
"path={blk_rate_limiter_test_img},num_queues={num_queues},bw_size={bw_size},bw_one_time_burst=0,bw_refill_time={bw_refill_time},image_type=raw"
)
} else {
format!(
"path={blk_rate_limiter_test_img},num_queues={num_queues},ops_size={bw_size},ops_one_time_burst=0,ops_refill_time={bw_refill_time},image_type=raw"
)
};
let mut child = GuestCommand::new(&guest)
.args(["--cpus", &format!("boot={num_queues}")])
.args(["--memory", "size=1G"])
.args(["--kernel", direct_kernel_boot_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(),
test_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();
let fio_command = format!(
"sudo fio --filename=/dev/vdc --name=test --output-format=json \
--direct=1 --bs=4k --ioengine=io_uring --iodepth=64 \
--rw={fio_ops} --runtime={BLOCK_RATE_LIMITER_RUNTIME} \
--ramp_time={BLOCK_RATE_LIMITER_RAMP_TIME} --numjobs={num_queues}",
);
let output = guest.ssh_command(&fio_command).unwrap();
// Parse fio output
let measured_rate = if bandwidth {
parse_fio_output(&output, &fio_ops, num_queues).unwrap()
} else {
parse_fio_output_iops(&output, &fio_ops, num_queues).unwrap()
};
assert!(check_rate_limit(measured_rate, limit_rate, 0.1));
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
fn _test_rate_limiter_group_block(bandwidth: bool, num_queues: u32, num_disks: u32) {
let fio_ops = FioOps::RandRW;
let bw_size = if bandwidth {
104857600_u64 // bytes
} else {
1000_u64 // I/O
};
let bw_refill_time = 1000; // ms
let limit_rate = (bw_size * 1000) as f64 / bw_refill_time as f64;
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 test_img_dir = TempDir::new_with_prefix("/var/tmp/ch").unwrap();
let rate_limit_group_arg = if bandwidth {
format!(
"id=group0,bw_size={bw_size},bw_one_time_burst=0,bw_refill_time={bw_refill_time}"
)
} else {
format!(
"id=group0,ops_size={bw_size},ops_one_time_burst=0,ops_refill_time={bw_refill_time}"
)
};
let mut disk_args = vec![
"--disk".to_string(),
format!(
"path={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
),
format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
),
];
for i in 0..num_disks {
let test_img_path = String::from(
test_img_dir
.as_path()
.join(format!("blk{i}.img"))
.to_str()
.unwrap(),
);
assert!(
exec_host_command_output(&format!(
"dd if=/dev/zero of={test_img_path} bs=1M count=1024"
))
.status
.success()
);
disk_args.push(format!(
"path={test_img_path},num_queues={num_queues},rate_limit_group=group0,image_type=raw"
));
}
let mut child = GuestCommand::new(&guest)
.args(["--cpus", &format!("boot={}", num_queues * num_disks)])
.args(["--memory", "size=1G"])
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args(["--rate-limit-group", &rate_limit_group_arg])
.args(disk_args)
.default_net()
.args(["--api-socket", &api_socket])
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
let mut fio_command = format!(
"sudo fio --name=global --output-format=json \
--direct=1 --bs=4k --ioengine=io_uring --iodepth=64 \
--rw={fio_ops} --runtime={BLOCK_RATE_LIMITER_RUNTIME} \
--ramp_time={BLOCK_RATE_LIMITER_RAMP_TIME} --numjobs={num_queues}",
);
// Generate additional argument for each disk:
// --name=job0 --filename=/dev/vdc \
// --name=job1 --filename=/dev/vdd \
// --name=job2 --filename=/dev/vde \
// ...
for i in 0..num_disks {
let c: char = 'c';
let arg = format!(
" --name=job{i} --filename=/dev/vd{}",
char::from_u32((c as u32) + i).unwrap()
);
fio_command += &arg;
}
let output = guest.ssh_command(&fio_command).unwrap();
// Parse fio output
let measured_rate = if bandwidth {
parse_fio_output(&output, &fio_ops, num_queues * num_disks).unwrap()
} else {
parse_fio_output_iops(&output, &fio_ops, num_queues * num_disks).unwrap()
};
assert!(check_rate_limit(measured_rate, limit_rate, 0.2));
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_rate_limiter_block_bandwidth() {
_test_rate_limiter_block(true, 1);
_test_rate_limiter_block(true, 2);
}
#[test]
fn test_rate_limiter_group_block_bandwidth() {
_test_rate_limiter_group_block(true, 1, 1);
_test_rate_limiter_group_block(true, 2, 1);
_test_rate_limiter_group_block(true, 1, 2);
_test_rate_limiter_group_block(true, 2, 2);
}
#[test]
fn test_rate_limiter_block_iops() {
_test_rate_limiter_block(false, 1);
_test_rate_limiter_block(false, 2);
}
#[test]
fn test_rate_limiter_group_block_iops() {
_test_rate_limiter_group_block(false, 1, 1);
_test_rate_limiter_group_block(false, 2, 1);
_test_rate_limiter_group_block(false, 1, 2);
_test_rate_limiter_group_block(false, 2, 2);
}
}
#[cfg(not(target_arch = "riscv64"))]
mod fw_cfg {
use crate::*;
#[test]
#[cfg_attr(feature = "mshv", ignore = "See #7434")]
fn test_fw_cfg() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let mut cmd = GuestCommand::new(&guest);
let kernel_path = direct_kernel_boot_path();
let cmd_line = DIRECT_KERNEL_BOOT_CMDLINE;
let test_file = guest.tmp_dir.as_path().join("test-file");
std::fs::write(&test_file, "test-file-content").unwrap();
cmd.args(["--cpus", "boot=4"])
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", cmd_line])
.default_disks()
.default_net()
.args([
"--fw-cfg-config",
&format!(
"initramfs=off,items=[name=opt/org.test/test-file,file={}]",
test_file.to_str().unwrap()
),
])
.capture_output();
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
// Wait a while for guest
thread::sleep(std::time::Duration::new(3, 0));
let result = guest
.ssh_command(
"sudo cat /sys/firmware/qemu_fw_cfg/by_name/opt/org.test/test-file/raw",
)
.unwrap();
assert_eq!(result, "test-file-content");
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
#[cfg_attr(feature = "mshv", ignore = "See #7434")]
fn test_fw_cfg_string() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let mut cmd = GuestCommand::new(&guest);
let kernel_path = direct_kernel_boot_path();
let cmd_line = DIRECT_KERNEL_BOOT_CMDLINE;
cmd.args(["--cpus", "boot=4"])
.default_memory()
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", cmd_line])
.default_disks()
.default_net()
.args([
"--fw-cfg-config",
"initramfs=off,items=[name=opt/org.test/test-string,string=hello-from-vmm]",
])
.capture_output();
let mut child = cmd.spawn().unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
thread::sleep(std::time::Duration::new(3, 0));
let result = guest
.ssh_command(
"sudo cat /sys/firmware/qemu_fw_cfg/by_name/opt/org.test/test-string/raw",
)
.unwrap();
assert_eq!(result, "hello-from-vmm");
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
}