mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
This is a niche feature and we were overly testing it. Let's just switch to two tests. One for live migration and one for plain watchdog. This will reduce the CI time. As we are now running these tests sequentially we can also reduce some the delays in the tests. Signed-off-by: Rob Bradford <rbradford@meta.com>
11576 lines
409 KiB
Rust
11576 lines
409 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_jammy_hypervisor_fw() {
|
|
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME)
|
|
.with_kernel(fw_path(FwType::RustHypervisorFirmware));
|
|
_test_simple_launch(&guest);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_jammy_ovmf() {
|
|
let guest = basic_regular_guest!(JAMMY_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 disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
|
|
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(), JAMMY_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(), JAMMY_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(), JAMMY_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(JAMMY_IMAGE_NAME);
|
|
vhd_file_path.push(JAMMY_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(),
|
|
JAMMY_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(JAMMY_IMAGE_NAME);
|
|
vhdx_file_path.push(JAMMY_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(),
|
|
JAMMY_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");
|
|
|
|
// Prepare a separate cloud-init for the L2 guest with its own
|
|
// boot notification port.
|
|
let (_l2_ci_dir, l2_ci_path) = guest.prepare_l2_cloudinit();
|
|
rate_limited_copy(l2_ci_path, &cloud_init_vfio_base_path)
|
|
.expect("copying of L2 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();
|
|
GuestNetworkConfig::wait_vm_boot_from(
|
|
guest.network.l2_tcp_listener_port,
|
|
&guest.network.l2_guest_ip2,
|
|
DEFAULT_TCP_LISTENER_TIMEOUT,
|
|
)
|
|
.unwrap();
|
|
|
|
let auth = PasswordAuth {
|
|
username: String::from("cloud"),
|
|
password: String::from("cloud123"),
|
|
};
|
|
|
|
// 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() {
|
|
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 = "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>) {
|
|
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 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_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(JAMMY_IMAGE_NAME.to_string());
|
|
let guest1 = Guest::new(Box::new(disk_config1));
|
|
|
|
let disk_config2 = UbuntuDiskConfig::new(JAMMY_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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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"))]
|
|
#[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),
|
|
));
|
|
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;
|
|
|
|
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(JAMMY_IMAGE_NAME.to_string());
|
|
let ovs_guest = Guest::new(Box::new(ovs_disk_config));
|
|
|
|
let migration_disk_config = UbuntuDiskConfig::new(JAMMY_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);
|
|
}
|
|
|
|
#[cfg(not(feature = "mshv"))]
|
|
fn _test_live_migration_watchdog(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=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(120).unwrap();
|
|
// Check a reboot is triggered by the watchdog
|
|
expected_reboot_count += 1;
|
|
assert_eq!(get_reboot_count(&guest), expected_reboot_count);
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
// Now pause the VM and remain offline for 30s
|
|
assert!(remote_command(&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);
|
|
}
|
|
|
|
#[test]
|
|
fn test_watchdog() {
|
|
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
|
|
_test_watchdog(&guest);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(not(feature = "mshv"))]
|
|
fn test_live_migration_watchdog() {
|
|
_test_live_migration_watchdog(false, false);
|
|
}
|
|
}
|
|
|
|
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";
|
|
const IORESOURCE_MEM: u64 = 0x0000_0200;
|
|
const IORESOURCE_PREFETCH: u64 = 0x0000_2000;
|
|
|
|
fn nvidia_vfio_device_ready() -> bool {
|
|
if !std::path::Path::new(NVIDIA_VFIO_DEVICE).exists() {
|
|
println!("SKIPPED: VFIO device {NVIDIA_VFIO_DEVICE} not found");
|
|
return false;
|
|
}
|
|
|
|
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 false;
|
|
}
|
|
} else {
|
|
println!("SKIPPED: VFIO device {NVIDIA_VFIO_DEVICE} not bound to any driver");
|
|
return false;
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
fn largest_nvidia_prefetchable_memory_bar() -> Option<u8> {
|
|
let resource_path = format!("{NVIDIA_VFIO_DEVICE}/resource");
|
|
let resource = match std::fs::read_to_string(&resource_path) {
|
|
Ok(resource) => resource,
|
|
Err(e) => {
|
|
println!("SKIPPED: failed to read {resource_path}: {e}");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let mut selected_bar = None;
|
|
let mut selected_size = 0;
|
|
for (index, line) in resource.lines().take(6).enumerate() {
|
|
let mut fields = line.split_whitespace();
|
|
let Some(start) = fields.next() else {
|
|
continue;
|
|
};
|
|
let Some(end) = fields.next() else {
|
|
continue;
|
|
};
|
|
let Some(flags) = fields.next() else {
|
|
continue;
|
|
};
|
|
|
|
let parse_hex = |value: &str| u64::from_str_radix(value.trim_start_matches("0x"), 16);
|
|
let Ok(start) = parse_hex(start) else {
|
|
continue;
|
|
};
|
|
let Ok(end) = parse_hex(end) else {
|
|
continue;
|
|
};
|
|
let Ok(flags) = parse_hex(flags) else {
|
|
continue;
|
|
};
|
|
|
|
if flags & IORESOURCE_MEM == 0 || end < start || (start == 0 && end == 0) {
|
|
continue;
|
|
}
|
|
if flags & IORESOURCE_PREFETCH == 0 {
|
|
continue;
|
|
}
|
|
|
|
let size = end - start + 1;
|
|
if size > selected_size {
|
|
selected_bar = Some(index as u8);
|
|
selected_size = size;
|
|
}
|
|
}
|
|
|
|
if selected_bar.is_none() {
|
|
println!(
|
|
"SKIPPED: no non-empty prefetchable memory BAR found for {NVIDIA_VFIO_DEVICE}"
|
|
);
|
|
}
|
|
selected_bar
|
|
}
|
|
|
|
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_card_x_exclude_mmap_bars_common(iommufd: bool) {
|
|
if !nvidia_vfio_device_ready() {
|
|
return;
|
|
}
|
|
|
|
let Some(bar) = largest_nvidia_prefetchable_memory_bar() else {
|
|
return;
|
|
};
|
|
|
|
let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(disk_config));
|
|
|
|
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([
|
|
"--device",
|
|
format!("path={NVIDIA_VFIO_DEVICE},x_exclude_mmap_bars=[{bar}]").as_str(),
|
|
])
|
|
.default_disks()
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot().unwrap();
|
|
assert!(wait_until(Duration::from_secs(10), || guest.check_nvidia_gpu()));
|
|
});
|
|
|
|
let _ = child.kill();
|
|
let output = child.wait_with_output().unwrap();
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
assert!(
|
|
stderr.contains("Skipping VFIO BAR mmap"),
|
|
"Expected x_exclude_mmap_bars log in stderr: {stderr}"
|
|
);
|
|
assert!(
|
|
stderr.contains(format!("BAR {bar}").as_str()),
|
|
"Expected skipped BAR index in stderr: {stderr}"
|
|
);
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_nvidia_card_x_exclude_mmap_bars() {
|
|
test_nvidia_card_x_exclude_mmap_bars_common(false);
|
|
}
|
|
|
|
#[test]
|
|
fn test_iommufd_nvidia_card_x_exclude_mmap_bars() {
|
|
test_nvidia_card_x_exclude_mmap_bars_common(true);
|
|
}
|
|
|
|
fn test_nvidia_guest_numa_generic_initiator_common(iommufd: bool) {
|
|
if !nvidia_vfio_device_ready() {
|
|
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 jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
|
|
|
|
vec![Box::new(jammy)].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);
|
|
}
|
|
}
|