The rate-limiter worker now is running on Azure VMs whose performance are more prone to be fluctuate, particularly on block performances. This commit relaxes the limit check for group rate limiter tests to make them less flaky. Signed-off-by: Bo Chen <chen.bo@intel.com>
10788 lines
371 KiB
Rust
10788 lines
371 KiB
Rust
// Copyright © 2020 Intel Corporation
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
#![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)]
|
|
|
|
extern crate test_infra;
|
|
|
|
use net_util::MacAddr;
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::io;
|
|
use std::io::BufRead;
|
|
use std::io::Read;
|
|
use std::io::Seek;
|
|
use std::io::Write;
|
|
use std::os::unix::io::AsRawFd;
|
|
use std::path::PathBuf;
|
|
use std::process::{Child, Command, Stdio};
|
|
use std::string::String;
|
|
use std::sync::mpsc;
|
|
use std::sync::mpsc::Receiver;
|
|
use std::sync::Mutex;
|
|
use std::thread;
|
|
use test_infra::*;
|
|
use vmm_sys_util::{tempdir::TempDir, tempfile::TempFile};
|
|
use wait_timeout::ChildExt;
|
|
|
|
// Constant taken from the VMM crate.
|
|
const MAX_NUM_PCI_SEGMENTS: u16 = 96;
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
mod x86_64 {
|
|
pub const FOCAL_IMAGE_NAME: &str = "focal-server-cloudimg-amd64-custom-20210609-0.raw";
|
|
pub const JAMMY_NVIDIA_IMAGE_NAME: &str = "jammy-server-cloudimg-amd64-nvidia.raw";
|
|
pub const FOCAL_IMAGE_NAME_QCOW2: &str = "focal-server-cloudimg-amd64-custom-20210609-0.qcow2";
|
|
pub const FOCAL_IMAGE_NAME_QCOW2_BACKING_FILE: &str =
|
|
"focal-server-cloudimg-amd64-custom-20210609-0-backing.qcow2";
|
|
pub const FOCAL_IMAGE_NAME_VHD: &str = "focal-server-cloudimg-amd64-custom-20210609-0.vhd";
|
|
pub const FOCAL_IMAGE_NAME_VHDX: &str = "focal-server-cloudimg-amd64-custom-20210609-0.vhdx";
|
|
pub const JAMMY_IMAGE_NAME: &str = "jammy-server-cloudimg-amd64-custom-20230119-0.raw";
|
|
pub const WINDOWS_IMAGE_NAME: &str = "windows-server-2022-amd64-2.raw";
|
|
pub const OVMF_NAME: &str = "CLOUDHV.fd";
|
|
pub const GREP_SERIAL_IRQ_CMD: &str = "grep -c 'IO-APIC.*ttyS0' /proc/interrupts || true";
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
use x86_64::*;
|
|
|
|
#[cfg(target_arch = "aarch64")]
|
|
mod aarch64 {
|
|
pub const FOCAL_IMAGE_NAME: &str = "focal-server-cloudimg-arm64-custom-20210929-0.raw";
|
|
pub const FOCAL_IMAGE_UPDATE_KERNEL_NAME: &str =
|
|
"focal-server-cloudimg-arm64-custom-20210929-0-update-kernel.raw";
|
|
pub const FOCAL_IMAGE_NAME_QCOW2: &str = "focal-server-cloudimg-arm64-custom-20210929-0.qcow2";
|
|
pub const FOCAL_IMAGE_NAME_QCOW2_BACKING_FILE: &str =
|
|
"focal-server-cloudimg-arm64-custom-20210929-0-backing.qcow2";
|
|
pub const FOCAL_IMAGE_NAME_VHD: &str = "focal-server-cloudimg-arm64-custom-20210929-0.vhd";
|
|
pub const FOCAL_IMAGE_NAME_VHDX: &str = "focal-server-cloudimg-arm64-custom-20210929-0.vhdx";
|
|
pub const JAMMY_IMAGE_NAME: &str = "jammy-server-cloudimg-arm64-custom-20220329-0.raw";
|
|
pub const WINDOWS_IMAGE_NAME: &str = "windows-11-iot-enterprise-aarch64.raw";
|
|
pub const OVMF_NAME: &str = "CLOUDHV_EFI.fd";
|
|
pub const GREP_SERIAL_IRQ_CMD: &str = "grep -c 'GICv3.*uart-pl011' /proc/interrupts || true";
|
|
pub const GREP_PMU_IRQ_CMD: &str = "grep -c 'GICv3.*arm-pmu' /proc/interrupts || true";
|
|
}
|
|
|
|
#[cfg(target_arch = "aarch64")]
|
|
use aarch64::*;
|
|
|
|
const DIRECT_KERNEL_BOOT_CMDLINE: &str =
|
|
"root=/dev/vda1 console=hvc0 rw systemd.journald.forward_to_console=1";
|
|
|
|
const CONSOLE_TEST_STRING: &str = "Started OpenBSD Secure Shell server";
|
|
|
|
// This enum exists to make it more convenient to
|
|
// implement test for both D-Bus and REST APIs.
|
|
enum TargetApi {
|
|
// API socket
|
|
HttpApi(String),
|
|
// well known service name, object path
|
|
DBusApi(String, String),
|
|
}
|
|
|
|
impl TargetApi {
|
|
fn new_http_api(tmp_dir: &TempDir) -> Self {
|
|
Self::HttpApi(temp_api_path(tmp_dir))
|
|
}
|
|
|
|
fn new_dbus_api(tmp_dir: &TempDir) -> Self {
|
|
// `tmp_dir` is in the form of "/tmp/chXXXXXX"
|
|
// and we take the `chXXXXXX` part as a unique identifier for the guest
|
|
let id = tmp_dir.as_path().file_name().unwrap().to_str().unwrap();
|
|
|
|
Self::DBusApi(
|
|
format!("org.cloudhypervisor.{id}"),
|
|
format!("/org/cloudhypervisor/{id}"),
|
|
)
|
|
}
|
|
|
|
fn guest_args(&self) -> Vec<String> {
|
|
match self {
|
|
TargetApi::HttpApi(api_socket) => {
|
|
vec![format!("--api-socket={}", api_socket.as_str())]
|
|
}
|
|
TargetApi::DBusApi(service_name, object_path) => {
|
|
vec![
|
|
format!("--dbus-service-name={}", service_name.as_str()),
|
|
format!("--dbus-object-path={}", object_path.as_str()),
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
fn remote_args(&self) -> Vec<String> {
|
|
// `guest_args` and `remote_args` are consistent with each other
|
|
self.guest_args()
|
|
}
|
|
|
|
fn remote_command(&self, command: &str, arg: Option<&str>) -> bool {
|
|
let mut cmd = Command::new(clh_command("ch-remote"));
|
|
cmd.args(self.remote_args());
|
|
cmd.arg(command);
|
|
|
|
if let Some(arg) = arg {
|
|
cmd.arg(arg);
|
|
}
|
|
|
|
let output = cmd.output().unwrap();
|
|
if output.status.success() {
|
|
true
|
|
} else {
|
|
eprintln!("Error running ch-remote command: {:?}", &cmd);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
eprintln!("stderr: {stderr}");
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start cloud-hypervisor with no VM parameters, only the API server running.
|
|
// From the API: Create a VM, boot it and check that it looks as expected.
|
|
fn _test_api_create_boot(target_api: TargetApi, guest: Guest) {
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(target_api.guest_args())
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
thread::sleep(std::time::Duration::new(1, 0));
|
|
|
|
// Verify API server is running
|
|
assert!(target_api.remote_command("ping", None));
|
|
|
|
// Create the VM first
|
|
let cpu_count: u8 = 4;
|
|
let request_body = guest.api_create_body(
|
|
cpu_count,
|
|
direct_kernel_boot_path().to_str().unwrap(),
|
|
DIRECT_KERNEL_BOOT_CMDLINE,
|
|
);
|
|
|
|
let temp_config_path = guest.tmp_dir.as_path().join("config");
|
|
std::fs::write(&temp_config_path, request_body).unwrap();
|
|
let create_config = temp_config_path.as_os_str().to_str().unwrap();
|
|
|
|
assert!(target_api.remote_command("create", Some(create_config),));
|
|
|
|
// Then boot it
|
|
assert!(target_api.remote_command("boot", None));
|
|
thread::sleep(std::time::Duration::new(20, 0));
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
// Check that the VM booted as expected
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count);
|
|
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);
|
|
}
|
|
|
|
// Start cloud-hypervisor with no VM parameters, only the API server running.
|
|
// From the API: Create a VM, boot it and check it can be shutdown and then
|
|
// booted again
|
|
fn _test_api_shutdown(target_api: TargetApi, guest: Guest) {
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(target_api.guest_args())
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
thread::sleep(std::time::Duration::new(1, 0));
|
|
|
|
// Verify API server is running
|
|
assert!(target_api.remote_command("ping", None));
|
|
|
|
// Create the VM first
|
|
let cpu_count: u8 = 4;
|
|
let request_body = guest.api_create_body(
|
|
cpu_count,
|
|
direct_kernel_boot_path().to_str().unwrap(),
|
|
DIRECT_KERNEL_BOOT_CMDLINE,
|
|
);
|
|
|
|
let temp_config_path = guest.tmp_dir.as_path().join("config");
|
|
std::fs::write(&temp_config_path, request_body).unwrap();
|
|
let create_config = temp_config_path.as_os_str().to_str().unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
assert!(target_api.remote_command("create", Some(create_config)));
|
|
|
|
// Then boot it
|
|
assert!(target_api.remote_command("boot", None));
|
|
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Check that the VM booted as expected
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count);
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
|
|
|
// 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
|
|
thread::sleep(std::time::Duration::new(20, 0));
|
|
|
|
// Then shut it down
|
|
assert!(target_api.remote_command("shutdown", None));
|
|
|
|
// Then boot it again
|
|
assert!(target_api.remote_command("boot", None));
|
|
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Check that the VM booted as expected
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count);
|
|
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);
|
|
}
|
|
|
|
// Start cloud-hypervisor with no VM parameters, only the API server running.
|
|
// From the API: Create a VM, boot it and check it can be deleted and then recreated
|
|
// booted again.
|
|
fn _test_api_delete(target_api: TargetApi, guest: Guest) {
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(target_api.guest_args())
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
thread::sleep(std::time::Duration::new(1, 0));
|
|
|
|
// Verify API server is running
|
|
assert!(target_api.remote_command("ping", None));
|
|
|
|
// Create the VM first
|
|
let cpu_count: u8 = 4;
|
|
let request_body = guest.api_create_body(
|
|
cpu_count,
|
|
direct_kernel_boot_path().to_str().unwrap(),
|
|
DIRECT_KERNEL_BOOT_CMDLINE,
|
|
);
|
|
let temp_config_path = guest.tmp_dir.as_path().join("config");
|
|
std::fs::write(&temp_config_path, request_body).unwrap();
|
|
let create_config = temp_config_path.as_os_str().to_str().unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
assert!(target_api.remote_command("create", Some(create_config)));
|
|
|
|
// Then boot it
|
|
assert!(target_api.remote_command("boot", None));
|
|
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Check that the VM booted as expected
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count);
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
|
|
|
// 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
|
|
thread::sleep(std::time::Duration::new(20, 0));
|
|
|
|
// Then delete it
|
|
assert!(target_api.remote_command("delete", None));
|
|
|
|
assert!(target_api.remote_command("create", Some(create_config)));
|
|
|
|
// Then boot it again
|
|
assert!(target_api.remote_command("boot", None));
|
|
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Check that the VM booted as expected
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count);
|
|
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);
|
|
}
|
|
|
|
// Start cloud-hypervisor with no VM parameters, only the API server running.
|
|
// From the API: Create a VM, boot it and check that it looks as expected.
|
|
// Then we pause the VM, check that it's no longer available.
|
|
// Finally we resume the VM and check that it's available.
|
|
fn _test_api_pause_resume(target_api: TargetApi, guest: Guest) {
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(target_api.guest_args())
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
thread::sleep(std::time::Duration::new(1, 0));
|
|
|
|
// Verify API server is running
|
|
assert!(target_api.remote_command("ping", None));
|
|
|
|
// Create the VM first
|
|
let cpu_count: u8 = 4;
|
|
let request_body = guest.api_create_body(
|
|
cpu_count,
|
|
direct_kernel_boot_path().to_str().unwrap(),
|
|
DIRECT_KERNEL_BOOT_CMDLINE,
|
|
);
|
|
|
|
let temp_config_path = guest.tmp_dir.as_path().join("config");
|
|
std::fs::write(&temp_config_path, request_body).unwrap();
|
|
let create_config = temp_config_path.as_os_str().to_str().unwrap();
|
|
|
|
assert!(target_api.remote_command("create", Some(create_config)));
|
|
|
|
// Then boot it
|
|
assert!(target_api.remote_command("boot", None));
|
|
thread::sleep(std::time::Duration::new(20, 0));
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
// Check that the VM booted as expected
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count);
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
|
|
|
// We now pause the VM
|
|
assert!(target_api.remote_command("pause", None));
|
|
|
|
// Check pausing again fails
|
|
assert!(!target_api.remote_command("pause", None));
|
|
|
|
thread::sleep(std::time::Duration::new(2, 0));
|
|
|
|
// SSH into the VM should fail
|
|
assert!(ssh_command_ip(
|
|
"grep -c processor /proc/cpuinfo",
|
|
&guest.network.guest_ip,
|
|
2,
|
|
5
|
|
)
|
|
.is_err());
|
|
|
|
// Resume the VM
|
|
assert!(target_api.remote_command("resume", None));
|
|
|
|
// Check resuming again fails
|
|
assert!(!target_api.remote_command("resume", None));
|
|
|
|
thread::sleep(std::time::Duration::new(2, 0));
|
|
|
|
// Now we should be able to SSH back in and get the right number of CPUs
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
fn _test_pty_interaction(pty_path: PathBuf) {
|
|
let mut cf = std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.read(true)
|
|
.open(pty_path)
|
|
.unwrap();
|
|
|
|
// Some dumb sleeps but we don't want to write
|
|
// before the console is up and we don't want
|
|
// to try and write the next line before the
|
|
// login process is ready.
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
assert_eq!(cf.write(b"cloud\n").unwrap(), 6);
|
|
thread::sleep(std::time::Duration::new(2, 0));
|
|
assert_eq!(cf.write(b"cloud123\n").unwrap(), 9);
|
|
thread::sleep(std::time::Duration::new(2, 0));
|
|
assert_eq!(cf.write(b"echo test_pty_console\n").unwrap(), 22);
|
|
thread::sleep(std::time::Duration::new(2, 0));
|
|
|
|
// read pty and ensure they have a login shell
|
|
// some fairly hacky workarounds to avoid looping
|
|
// forever in case the channel is blocked getting output
|
|
let ptyc = pty_read(cf);
|
|
let mut empty = 0;
|
|
let mut prev = String::new();
|
|
loop {
|
|
thread::sleep(std::time::Duration::new(2, 0));
|
|
match ptyc.try_recv() {
|
|
Ok(line) => {
|
|
empty = 0;
|
|
prev = prev + &line;
|
|
if prev.contains("test_pty_console") {
|
|
break;
|
|
}
|
|
}
|
|
Err(mpsc::TryRecvError::Empty) => {
|
|
empty += 1;
|
|
assert!(empty <= 5, "No login on pty");
|
|
}
|
|
_ => {
|
|
panic!("No login on pty")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn prepare_virtiofsd(tmp_dir: &TempDir, shared_dir: &str) -> (std::process::Child, String) {
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut virtiofsd_path = workload_path;
|
|
virtiofsd_path.push("virtiofsd");
|
|
let virtiofsd_path = String::from(virtiofsd_path.to_str().unwrap());
|
|
|
|
let virtiofsd_socket_path =
|
|
String::from(tmp_dir.as_path().join("virtiofs.sock").to_str().unwrap());
|
|
|
|
// Start the daemon
|
|
let child = Command::new(virtiofsd_path.as_str())
|
|
.args(["--shared-dir", shared_dir])
|
|
.args(["--socket-path", virtiofsd_socket_path.as_str()])
|
|
.args(["--cache", "never"])
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
|
|
(child, virtiofsd_socket_path)
|
|
}
|
|
|
|
fn prepare_vubd(
|
|
tmp_dir: &TempDir,
|
|
blk_img: &str,
|
|
num_queues: usize,
|
|
rdonly: bool,
|
|
direct: bool,
|
|
) -> (std::process::Child, String) {
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut blk_file_path = workload_path;
|
|
blk_file_path.push(blk_img);
|
|
let blk_file_path = String::from(blk_file_path.to_str().unwrap());
|
|
|
|
let vubd_socket_path = String::from(tmp_dir.as_path().join("vub.sock").to_str().unwrap());
|
|
|
|
// Start the daemon
|
|
let child = Command::new(clh_command("vhost_user_block"))
|
|
.args([
|
|
"--block-backend",
|
|
format!(
|
|
"path={blk_file_path},socket={vubd_socket_path},num_queues={num_queues},readonly={rdonly},direct={direct}"
|
|
)
|
|
.as_str(),
|
|
])
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
|
|
(child, vubd_socket_path)
|
|
}
|
|
|
|
fn temp_vsock_path(tmp_dir: &TempDir) -> String {
|
|
String::from(tmp_dir.as_path().join("vsock").to_str().unwrap())
|
|
}
|
|
|
|
fn temp_api_path(tmp_dir: &TempDir) -> String {
|
|
String::from(
|
|
tmp_dir
|
|
.as_path()
|
|
.join("cloud-hypervisor.sock")
|
|
.to_str()
|
|
.unwrap(),
|
|
)
|
|
}
|
|
|
|
fn temp_event_monitor_path(tmp_dir: &TempDir) -> String {
|
|
String::from(tmp_dir.as_path().join("event.json").to_str().unwrap())
|
|
}
|
|
|
|
// Creates the directory and returns the path.
|
|
fn temp_snapshot_dir_path(tmp_dir: &TempDir) -> String {
|
|
let snapshot_dir = String::from(tmp_dir.as_path().join("snapshot").to_str().unwrap());
|
|
std::fs::create_dir(&snapshot_dir).unwrap();
|
|
snapshot_dir
|
|
}
|
|
|
|
fn temp_vmcore_file_path(tmp_dir: &TempDir) -> String {
|
|
let vmcore_file = String::from(tmp_dir.as_path().join("vmcore").to_str().unwrap());
|
|
vmcore_file
|
|
}
|
|
|
|
// Creates the path for direct kernel boot and return the path.
|
|
// For x86_64, this function returns the vmlinux kernel path.
|
|
// For AArch64, this function returns the PE kernel path.
|
|
fn direct_kernel_boot_path() -> PathBuf {
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut kernel_path = workload_path;
|
|
#[cfg(target_arch = "x86_64")]
|
|
kernel_path.push("vmlinux");
|
|
#[cfg(target_arch = "aarch64")]
|
|
kernel_path.push("Image");
|
|
|
|
kernel_path
|
|
}
|
|
|
|
fn edk2_path() -> PathBuf {
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
let mut edk2_path = workload_path;
|
|
edk2_path.push(OVMF_NAME);
|
|
|
|
edk2_path
|
|
}
|
|
|
|
fn cloud_hypervisor_release_path() -> String {
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut ch_release_path = workload_path;
|
|
#[cfg(target_arch = "x86_64")]
|
|
ch_release_path.push("cloud-hypervisor-static");
|
|
#[cfg(target_arch = "aarch64")]
|
|
ch_release_path.push("cloud-hypervisor-static-aarch64");
|
|
|
|
ch_release_path.into_os_string().into_string().unwrap()
|
|
}
|
|
|
|
fn prepare_vhost_user_net_daemon(
|
|
tmp_dir: &TempDir,
|
|
ip: &str,
|
|
tap: Option<&str>,
|
|
mtu: Option<u16>,
|
|
num_queues: usize,
|
|
client_mode: bool,
|
|
) -> (std::process::Command, String) {
|
|
let vunet_socket_path = String::from(tmp_dir.as_path().join("vunet.sock").to_str().unwrap());
|
|
|
|
// Start the daemon
|
|
let mut net_params = format!(
|
|
"ip={ip},mask=255.255.255.0,socket={vunet_socket_path},num_queues={num_queues},queue_size=1024,client={client_mode}"
|
|
);
|
|
|
|
if let Some(tap) = tap {
|
|
net_params.push_str(format!(",tap={tap}").as_str());
|
|
}
|
|
|
|
if let Some(mtu) = mtu {
|
|
net_params.push_str(format!(",mtu={mtu}").as_str());
|
|
}
|
|
|
|
let mut command = Command::new(clh_command("vhost_user_net"));
|
|
command.args(["--net-backend", net_params.as_str()]);
|
|
|
|
(command, vunet_socket_path)
|
|
}
|
|
|
|
fn prepare_swtpm_daemon(tmp_dir: &TempDir) -> (std::process::Command, String) {
|
|
let swtpm_tpm_dir = String::from(tmp_dir.as_path().join("swtpm").to_str().unwrap());
|
|
let swtpm_socket_path = String::from(
|
|
tmp_dir
|
|
.as_path()
|
|
.join("swtpm")
|
|
.join("swtpm.sock")
|
|
.to_str()
|
|
.unwrap(),
|
|
);
|
|
std::fs::create_dir(&swtpm_tpm_dir).unwrap();
|
|
|
|
let mut swtpm_command = Command::new("swtpm");
|
|
let swtpm_args = [
|
|
"socket",
|
|
"--tpmstate",
|
|
&format!("dir={swtpm_tpm_dir}"),
|
|
"--ctrl",
|
|
&format!("type=unixio,path={swtpm_socket_path}"),
|
|
"--flags",
|
|
"startup-clear",
|
|
"--tpm2",
|
|
];
|
|
swtpm_command.args(swtpm_args);
|
|
|
|
(swtpm_command, swtpm_socket_path)
|
|
}
|
|
|
|
fn remote_command(api_socket: &str, command: &str, arg: Option<&str>) -> bool {
|
|
let mut cmd = Command::new(clh_command("ch-remote"));
|
|
cmd.args([&format!("--api-socket={api_socket}"), command]);
|
|
|
|
if let Some(arg) = arg {
|
|
cmd.arg(arg);
|
|
}
|
|
let output = cmd.output().unwrap();
|
|
if output.status.success() {
|
|
true
|
|
} else {
|
|
eprintln!("Error running ch-remote command: {:?}", &cmd);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
eprintln!("stderr: {stderr}");
|
|
false
|
|
}
|
|
}
|
|
|
|
fn remote_command_w_output(api_socket: &str, command: &str, arg: Option<&str>) -> (bool, Vec<u8>) {
|
|
let mut cmd = Command::new(clh_command("ch-remote"));
|
|
cmd.args([&format!("--api-socket={api_socket}"), command]);
|
|
|
|
if let Some(arg) = arg {
|
|
cmd.arg(arg);
|
|
}
|
|
|
|
let output = cmd.output().expect("Failed to launch ch-remote");
|
|
|
|
(output.status.success(), output.stdout)
|
|
}
|
|
|
|
fn resize_command(
|
|
api_socket: &str,
|
|
desired_vcpus: Option<u8>,
|
|
desired_ram: Option<usize>,
|
|
desired_balloon: Option<usize>,
|
|
event_file: Option<&str>,
|
|
) -> bool {
|
|
let mut cmd = Command::new(clh_command("ch-remote"));
|
|
cmd.args([&format!("--api-socket={api_socket}"), "resize"]);
|
|
|
|
if let Some(desired_vcpus) = desired_vcpus {
|
|
cmd.arg(format!("--cpus={desired_vcpus}"));
|
|
}
|
|
|
|
if let Some(desired_ram) = desired_ram {
|
|
cmd.arg(format!("--memory={desired_ram}"));
|
|
}
|
|
|
|
if let Some(desired_balloon) = desired_balloon {
|
|
cmd.arg(format!("--balloon={desired_balloon}"));
|
|
}
|
|
|
|
let ret = cmd.status().expect("Failed to launch ch-remote").success();
|
|
|
|
if let Some(event_path) = event_file {
|
|
let latest_events = [
|
|
&MetaEvent {
|
|
event: "resizing".to_string(),
|
|
device_id: None,
|
|
},
|
|
&MetaEvent {
|
|
event: "resized".to_string(),
|
|
device_id: None,
|
|
},
|
|
];
|
|
// See: #5938
|
|
thread::sleep(std::time::Duration::new(1, 0));
|
|
assert!(check_latest_events_exact(&latest_events, event_path));
|
|
}
|
|
|
|
ret
|
|
}
|
|
|
|
fn resize_zone_command(api_socket: &str, id: &str, desired_size: &str) -> bool {
|
|
let mut cmd = Command::new(clh_command("ch-remote"));
|
|
cmd.args([
|
|
&format!("--api-socket={api_socket}"),
|
|
"resize-zone",
|
|
&format!("--id={id}"),
|
|
&format!("--size={desired_size}"),
|
|
]);
|
|
|
|
cmd.status().expect("Failed to launch ch-remote").success()
|
|
}
|
|
|
|
// setup OVS-DPDK bridge and ports
|
|
fn setup_ovs_dpdk() {
|
|
// setup OVS-DPDK
|
|
assert!(exec_host_command_status("service openvswitch-switch start").success());
|
|
assert!(exec_host_command_status("ovs-vsctl init").success());
|
|
assert!(
|
|
exec_host_command_status("ovs-vsctl set Open_vSwitch . other_config:dpdk-init=true")
|
|
.success()
|
|
);
|
|
assert!(exec_host_command_status("service openvswitch-switch restart").success());
|
|
|
|
// Create OVS-DPDK bridge and ports
|
|
assert!(exec_host_command_status(
|
|
"ovs-vsctl add-br ovsbr0 -- set bridge ovsbr0 datapath_type=netdev",
|
|
)
|
|
.success());
|
|
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());
|
|
assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user2 -- set Interface vhost-user2 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient2").success());
|
|
assert!(exec_host_command_status("ip link set up dev ovsbr0").success());
|
|
assert!(exec_host_command_status("service openvswitch-switch restart").success());
|
|
}
|
|
fn cleanup_ovs_dpdk() {
|
|
assert!(exec_host_command_status("ovs-vsctl del-br ovsbr0").success());
|
|
exec_host_command_status("rm -f ovs-vsctl /tmp/dpdkvhostclient1 /tmp/dpdkvhostclient2");
|
|
}
|
|
// Setup two guests and ensure they are connected through ovs-dpdk
|
|
fn setup_ovs_dpdk_guests(
|
|
guest1: &Guest,
|
|
guest2: &Guest,
|
|
api_socket: &str,
|
|
release_binary: bool,
|
|
) -> (Child, Child) {
|
|
setup_ovs_dpdk();
|
|
|
|
let clh_path = if !release_binary {
|
|
clh_command("cloud-hypervisor")
|
|
} else {
|
|
cloud_hypervisor_release_path()
|
|
};
|
|
|
|
let mut child1 = GuestCommand::new_with_binary_path(guest1, &clh_path)
|
|
.args(["--cpus", "boot=2"])
|
|
.args(["--memory", "size=0,shared=on"])
|
|
.args(["--memory-zone", "id=mem0,size=1G,shared=on,host_numa_node=0"])
|
|
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.args(["--net", guest1.default_net_string().as_str(), "vhost_user=true,socket=/tmp/dpdkvhostclient1,num_queues=2,queue_size=256,vhost_mode=server"])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
let guest_net_iface = "ens5";
|
|
#[cfg(target_arch = "aarch64")]
|
|
let guest_net_iface = "enp0s5";
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest1.wait_vm_boot(None).unwrap();
|
|
|
|
guest1
|
|
.ssh_command(&format!(
|
|
"sudo ip addr add 172.100.0.1/24 dev {guest_net_iface}"
|
|
))
|
|
.unwrap();
|
|
guest1
|
|
.ssh_command(&format!("sudo ip link set up dev {guest_net_iface}"))
|
|
.unwrap();
|
|
|
|
let guest_ip = guest1.network.guest_ip.clone();
|
|
thread::spawn(move || {
|
|
ssh_command_ip(
|
|
"nc -l 12345",
|
|
&guest_ip,
|
|
DEFAULT_SSH_RETRIES,
|
|
DEFAULT_SSH_TIMEOUT,
|
|
)
|
|
.unwrap();
|
|
});
|
|
});
|
|
if r.is_err() {
|
|
cleanup_ovs_dpdk();
|
|
|
|
let _ = child1.kill();
|
|
let output = child1.wait_with_output().unwrap();
|
|
handle_child_output(r, &output);
|
|
panic!("Test should already be failed/panicked"); // To explicitly mark this block never return
|
|
}
|
|
|
|
let mut child2 = GuestCommand::new_with_binary_path(guest2, &clh_path)
|
|
.args(["--api-socket", api_socket])
|
|
.args(["--cpus", "boot=2"])
|
|
.args(["--memory", "size=0,shared=on"])
|
|
.args(["--memory-zone", "id=mem0,size=1G,shared=on,host_numa_node=0"])
|
|
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.args(["--net", guest2.default_net_string().as_str(), "vhost_user=true,socket=/tmp/dpdkvhostclient2,num_queues=2,queue_size=256,vhost_mode=server"])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest2.wait_vm_boot(None).unwrap();
|
|
|
|
guest2
|
|
.ssh_command(&format!(
|
|
"sudo ip addr add 172.100.0.2/24 dev {guest_net_iface}"
|
|
))
|
|
.unwrap();
|
|
guest2
|
|
.ssh_command(&format!("sudo ip link set up dev {guest_net_iface}"))
|
|
.unwrap();
|
|
|
|
// Check the connection works properly between the two VMs
|
|
guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap();
|
|
});
|
|
if r.is_err() {
|
|
cleanup_ovs_dpdk();
|
|
|
|
let _ = child1.kill();
|
|
let _ = child2.kill();
|
|
let output = child2.wait_with_output().unwrap();
|
|
handle_child_output(r, &output);
|
|
panic!("Test should already be failed/panicked"); // To explicitly mark this block never return
|
|
}
|
|
|
|
(child1, child2)
|
|
}
|
|
|
|
enum FwType {
|
|
Ovmf,
|
|
RustHypervisorFirmware,
|
|
}
|
|
|
|
fn fw_path(_fw_type: FwType) -> String {
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut fw_path = workload_path;
|
|
#[cfg(target_arch = "aarch64")]
|
|
fw_path.push("CLOUDHV_EFI.fd");
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
match _fw_type {
|
|
FwType::Ovmf => fw_path.push(OVMF_NAME),
|
|
FwType::RustHypervisorFirmware => fw_path.push("hypervisor-fw"),
|
|
}
|
|
}
|
|
|
|
fw_path.to_str().unwrap().to_string()
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct MetaEvent {
|
|
event: String,
|
|
device_id: Option<String>,
|
|
}
|
|
|
|
impl MetaEvent {
|
|
pub fn match_with_json_event(&self, v: &serde_json::Value) -> bool {
|
|
let mut matched = false;
|
|
if v["event"].as_str().unwrap() == self.event {
|
|
if let Some(device_id) = &self.device_id {
|
|
if v["properties"]["id"].as_str().unwrap() == device_id {
|
|
matched = true
|
|
}
|
|
} else {
|
|
matched = true;
|
|
}
|
|
}
|
|
matched
|
|
}
|
|
}
|
|
|
|
// Parse the event_monitor file based on the format that each event
|
|
// is followed by a double newline
|
|
fn parse_event_file(event_file: &str) -> Vec<serde_json::Value> {
|
|
let content = fs::read(event_file).unwrap();
|
|
let mut ret = Vec::new();
|
|
for entry in String::from_utf8_lossy(&content)
|
|
.trim()
|
|
.split("\n\n")
|
|
.collect::<Vec<&str>>()
|
|
{
|
|
ret.push(serde_json::from_str(entry).unwrap());
|
|
}
|
|
|
|
ret
|
|
}
|
|
|
|
// Return true if all events from the input 'expected_events' are matched sequentially
|
|
// with events from the 'event_file'
|
|
fn check_sequential_events(expected_events: &[&MetaEvent], event_file: &str) -> bool {
|
|
let json_events = parse_event_file(event_file);
|
|
let len = expected_events.len();
|
|
let mut idx = 0;
|
|
for e in &json_events {
|
|
if idx == len {
|
|
break;
|
|
}
|
|
if expected_events[idx].match_with_json_event(e) {
|
|
idx += 1;
|
|
}
|
|
}
|
|
|
|
let ret = idx == len;
|
|
|
|
if !ret {
|
|
eprintln!(
|
|
"\n\n==== Start 'check_sequential_events' failed ==== \
|
|
\n\nexpected_events={:?}\nactual_events={:?} \
|
|
\n\n==== End 'check_sequential_events' failed ====",
|
|
expected_events, json_events,
|
|
);
|
|
}
|
|
|
|
ret
|
|
}
|
|
|
|
// Return true if all events from the input 'expected_events' are matched exactly
|
|
// with events from the 'event_file'
|
|
fn check_sequential_events_exact(expected_events: &[&MetaEvent], event_file: &str) -> bool {
|
|
let json_events = parse_event_file(event_file);
|
|
assert!(expected_events.len() <= json_events.len());
|
|
let json_events = &json_events[..expected_events.len()];
|
|
|
|
for (idx, e) in json_events.iter().enumerate() {
|
|
if !expected_events[idx].match_with_json_event(e) {
|
|
eprintln!(
|
|
"\n\n==== Start 'check_sequential_events_exact' failed ==== \
|
|
\n\nexpected_events={:?}\nactual_events={:?} \
|
|
\n\n==== End 'check_sequential_events_exact' failed ====",
|
|
expected_events, json_events,
|
|
);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
// Return true if events from the input 'latest_events' are matched exactly
|
|
// with the most recent events from the 'event_file'
|
|
fn check_latest_events_exact(latest_events: &[&MetaEvent], event_file: &str) -> bool {
|
|
let json_events = parse_event_file(event_file);
|
|
assert!(latest_events.len() <= json_events.len());
|
|
let json_events = &json_events[(json_events.len() - latest_events.len())..];
|
|
|
|
for (idx, e) in json_events.iter().enumerate() {
|
|
if !latest_events[idx].match_with_json_event(e) {
|
|
eprintln!(
|
|
"\n\n==== Start 'check_latest_events_exact' failed ==== \
|
|
\n\nexpected_events={:?}\nactual_events={:?} \
|
|
\n\n==== End 'check_latest_events_exact' failed ====",
|
|
latest_events, json_events,
|
|
);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
fn test_cpu_topology(threads_per_core: u8, cores_per_package: u8, packages: u8, use_fw: bool) {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let total_vcpus = threads_per_core * cores_per_package * packages;
|
|
let direct_kernel_boot_path = direct_kernel_boot_path();
|
|
let mut kernel_path = direct_kernel_boot_path.to_str().unwrap();
|
|
let fw_path = fw_path(FwType::RustHypervisorFirmware);
|
|
if use_fw {
|
|
kernel_path = fw_path.as_str();
|
|
}
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args([
|
|
"--cpus",
|
|
&format!(
|
|
"boot={total_vcpus},topology={threads_per_core}:{cores_per_package}:1:{packages}"
|
|
),
|
|
])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", kernel_path])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
assert_eq!(
|
|
guest.get_cpu_count().unwrap_or_default(),
|
|
u32::from(total_vcpus)
|
|
);
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lscpu | grep \"per core\" | cut -f 2 -d \":\" | sed \"s# *##\"")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u8>()
|
|
.unwrap_or(0),
|
|
threads_per_core
|
|
);
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lscpu | grep \"per socket\" | cut -f 2 -d \":\" | sed \"s# *##\"")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u8>()
|
|
.unwrap_or(0),
|
|
cores_per_package
|
|
);
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lscpu | grep \"Socket\" | cut -f 2 -d \":\" | sed \"s# *##\"")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u8>()
|
|
.unwrap_or(0),
|
|
packages
|
|
);
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
let mut cpu_id = 0;
|
|
for package_id in 0..packages {
|
|
for core_id in 0..cores_per_package {
|
|
for _ in 0..threads_per_core {
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(&format!("cat /sys/devices/system/cpu/cpu{cpu_id}/topology/physical_package_id"))
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u8>()
|
|
.unwrap_or(0),
|
|
package_id
|
|
);
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(&format!(
|
|
"cat /sys/devices/system/cpu/cpu{cpu_id}/topology/core_id"
|
|
))
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u8>()
|
|
.unwrap_or(0),
|
|
core_id
|
|
);
|
|
|
|
cpu_id += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[allow(unused_variables)]
|
|
fn _test_guest_numa_nodes(acpi: bool) {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let api_socket = temp_api_path(&guest.tmp_dir);
|
|
#[cfg(target_arch = "x86_64")]
|
|
let kernel_path = direct_kernel_boot_path();
|
|
#[cfg(target_arch = "aarch64")]
|
|
let kernel_path = if acpi {
|
|
edk2_path()
|
|
} else {
|
|
direct_kernel_boot_path()
|
|
};
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=6,max=12"])
|
|
.args(["--memory", "size=0,hotplug_method=virtio-mem"])
|
|
.args([
|
|
"--memory-zone",
|
|
"id=mem0,size=1G,hotplug_size=3G",
|
|
"id=mem1,size=2G,hotplug_size=3G",
|
|
"id=mem2,size=3G,hotplug_size=3G",
|
|
])
|
|
.args([
|
|
"--numa",
|
|
"guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0",
|
|
"guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1",
|
|
"guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2",
|
|
])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.args(["--api-socket", &api_socket])
|
|
.capture_output()
|
|
.default_disks()
|
|
.default_net()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
guest.check_numa_common(
|
|
Some(&[960_000, 1_920_000, 2_880_000]),
|
|
Some(&[vec![0, 1, 2], vec![3, 4], vec![5]]),
|
|
Some(&["10 15 20", "20 10 25", "25 30 10"]),
|
|
);
|
|
|
|
// AArch64 currently does not support hotplug, and therefore we only
|
|
// test hotplug-related function on x86_64 here.
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
guest.enable_memory_hotplug();
|
|
|
|
// Resize every memory zone and check each associated NUMA node
|
|
// has been assigned the right amount of memory.
|
|
resize_zone_command(&api_socket, "mem0", "4G");
|
|
resize_zone_command(&api_socket, "mem1", "4G");
|
|
resize_zone_command(&api_socket, "mem2", "4G");
|
|
// Resize to the maximum amount of CPUs and check each NUMA
|
|
// node has been assigned the right CPUs set.
|
|
resize_command(&api_socket, Some(12), None, None, None);
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
|
|
guest.check_numa_common(
|
|
Some(&[3_840_000, 3_840_000, 3_840_000]),
|
|
Some(&[vec![0, 1, 2, 9], vec![3, 4, 6, 7, 8], vec![5, 10, 11]]),
|
|
None,
|
|
);
|
|
}
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[allow(unused_variables)]
|
|
fn _test_power_button(acpi: bool) {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let mut cmd = GuestCommand::new(&guest);
|
|
let api_socket = temp_api_path(&guest.tmp_dir);
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
let kernel_path = direct_kernel_boot_path();
|
|
#[cfg(target_arch = "aarch64")]
|
|
let kernel_path = if acpi {
|
|
edk2_path()
|
|
} else {
|
|
direct_kernel_boot_path()
|
|
};
|
|
|
|
cmd.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.capture_output()
|
|
.default_disks()
|
|
.default_net()
|
|
.args(["--api-socket", &api_socket]);
|
|
|
|
let child = cmd.spawn().unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
assert!(remote_command(&api_socket, "power-button", None));
|
|
});
|
|
|
|
let output = child.wait_with_output().unwrap();
|
|
assert!(output.status.success());
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
type PrepareNetDaemon = dyn Fn(
|
|
&TempDir,
|
|
&str,
|
|
Option<&str>,
|
|
Option<u16>,
|
|
usize,
|
|
bool,
|
|
) -> (std::process::Command, String);
|
|
|
|
fn test_vhost_user_net(
|
|
tap: Option<&str>,
|
|
num_queues: usize,
|
|
prepare_daemon: &PrepareNetDaemon,
|
|
generate_host_mac: bool,
|
|
client_mode_daemon: bool,
|
|
) {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let api_socket = temp_api_path(&guest.tmp_dir);
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
let host_mac = if generate_host_mac {
|
|
Some(MacAddr::local_random())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let mtu = Some(3000);
|
|
|
|
let (mut daemon_command, vunet_socket_path) = prepare_daemon(
|
|
&guest.tmp_dir,
|
|
&guest.network.host_ip,
|
|
tap,
|
|
mtu,
|
|
num_queues,
|
|
client_mode_daemon,
|
|
);
|
|
|
|
let net_params = format!(
|
|
"vhost_user=true,mac={},socket={},num_queues={},queue_size=1024{},vhost_mode={},mtu=3000",
|
|
guest.network.guest_mac,
|
|
vunet_socket_path,
|
|
num_queues,
|
|
if let Some(host_mac) = host_mac {
|
|
format!(",host_mac={host_mac}")
|
|
} else {
|
|
"".to_owned()
|
|
},
|
|
if client_mode_daemon {
|
|
"server"
|
|
} else {
|
|
"client"
|
|
},
|
|
);
|
|
|
|
let mut ch_command = GuestCommand::new(&guest);
|
|
ch_command
|
|
.args(["--cpus", format!("boot={}", num_queues / 2).as_str()])
|
|
.args(["--memory", "size=512M,hotplug_size=2048M,shared=on"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.args(["--net", net_params.as_str()])
|
|
.args(["--api-socket", &api_socket])
|
|
.capture_output();
|
|
|
|
let mut daemon_child: std::process::Child;
|
|
let mut child: std::process::Child;
|
|
|
|
if client_mode_daemon {
|
|
child = ch_command.spawn().unwrap();
|
|
// Make sure the VMM is waiting for the backend to connect
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
daemon_child = daemon_command.spawn().unwrap();
|
|
} else {
|
|
daemon_child = daemon_command.spawn().unwrap();
|
|
// Make sure the backend is waiting for the VMM to connect
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
child = ch_command.spawn().unwrap();
|
|
}
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
if let Some(tap_name) = tap {
|
|
let tap_count = exec_host_command_output(&format!("ip link | grep -c {tap_name}"));
|
|
assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1");
|
|
}
|
|
|
|
if let Some(host_mac) = tap {
|
|
let mac_count = exec_host_command_output(&format!("ip link | grep -c {host_mac}"));
|
|
assert_eq!(String::from_utf8_lossy(&mac_count.stdout).trim(), "1");
|
|
}
|
|
|
|
#[cfg(target_arch = "aarch64")]
|
|
let iface = "enp0s4";
|
|
#[cfg(target_arch = "x86_64")]
|
|
let iface = "ens4";
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(format!("cat /sys/class/net/{iface}/mtu").as_str())
|
|
.unwrap()
|
|
.trim(),
|
|
"3000"
|
|
);
|
|
|
|
// 1 network interface + default localhost ==> 2 interfaces
|
|
// It's important to note that this test is fully exercising the
|
|
// vhost-user-net implementation and the associated backend since
|
|
// it does not define any --net network interface. That means all
|
|
// the ssh communication in that test happens through the network
|
|
// interface backed by vhost-user-net.
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("ip -o link | wc -l")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
2
|
|
);
|
|
|
|
// The following pci devices will appear on guest with PCI-MSI
|
|
// interrupt vectors assigned.
|
|
// 1 virtio-console with 3 vectors: config, Rx, Tx
|
|
// 1 virtio-blk with 2 vectors: config, Request
|
|
// 1 virtio-blk with 2 vectors: config, Request
|
|
// 1 virtio-rng with 2 vectors: config, Request
|
|
// Since virtio-net has 2 queue pairs, its vectors is as follows:
|
|
// 1 virtio-net with 5 vectors: config, Rx (2), Tx (2)
|
|
// Based on the above, the total vectors should 14.
|
|
#[cfg(target_arch = "x86_64")]
|
|
let grep_cmd = "grep -c PCI-MSI /proc/interrupts";
|
|
#[cfg(target_arch = "aarch64")]
|
|
let grep_cmd = "grep -c ITS-MSI /proc/interrupts";
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(grep_cmd)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
10 + (num_queues as u32)
|
|
);
|
|
|
|
// ACPI feature is needed.
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
guest.enable_memory_hotplug();
|
|
|
|
// Add RAM to the VM
|
|
let desired_ram = 1024 << 20;
|
|
resize_command(&api_socket, None, Some(desired_ram), None, None);
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
|
|
// Here by simply checking the size (through ssh), we validate
|
|
// the connection is still working, which means vhost-user-net
|
|
// keeps working after the resize.
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 960_000);
|
|
}
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
let _ = daemon_child.kill();
|
|
let _ = daemon_child.wait();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
type PrepareBlkDaemon = dyn Fn(&TempDir, &str, usize, bool, bool) -> (std::process::Child, String);
|
|
|
|
fn test_vhost_user_blk(
|
|
num_queues: usize,
|
|
readonly: bool,
|
|
direct: bool,
|
|
prepare_vhost_user_blk_daemon: Option<&PrepareBlkDaemon>,
|
|
) {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let api_socket = temp_api_path(&guest.tmp_dir);
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
let (blk_params, daemon_child) = {
|
|
let prepare_daemon = prepare_vhost_user_blk_daemon.unwrap();
|
|
// Start the daemon
|
|
let (daemon_child, vubd_socket_path) =
|
|
prepare_daemon(&guest.tmp_dir, "blk.img", num_queues, readonly, direct);
|
|
|
|
(
|
|
format!(
|
|
"vhost_user=true,socket={vubd_socket_path},num_queues={num_queues},queue_size=128",
|
|
),
|
|
Some(daemon_child),
|
|
)
|
|
};
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", format!("boot={num_queues}").as_str()])
|
|
.args(["--memory", "size=512M,hotplug_size=2048M,shared=on"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.args([
|
|
"--disk",
|
|
format!(
|
|
"path={}",
|
|
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
|
|
)
|
|
.as_str(),
|
|
format!(
|
|
"path={}",
|
|
guest.disk_config.disk(DiskType::CloudInit).unwrap()
|
|
)
|
|
.as_str(),
|
|
blk_params.as_str(),
|
|
])
|
|
.default_net()
|
|
.args(["--api-socket", &api_socket])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Check both if /dev/vdc exists and if the block size is 16M.
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep vdc | grep -c 16M")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
1
|
|
);
|
|
|
|
// Check if this block is RO or RW.
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep vdc | awk '{print $5}'")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
readonly as u32
|
|
);
|
|
|
|
// Check if the number of queues in /sys/block/vdc/mq matches the
|
|
// expected num_queues.
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
num_queues as u32
|
|
);
|
|
|
|
// Mount the device
|
|
let mount_ro_rw_flag = if readonly { "ro,noload" } else { "rw" };
|
|
guest.ssh_command("mkdir mount_image").unwrap();
|
|
guest
|
|
.ssh_command(
|
|
format!("sudo mount -o {mount_ro_rw_flag} -t ext4 /dev/vdc mount_image/").as_str(),
|
|
)
|
|
.unwrap();
|
|
|
|
// Check the content of the block device. The file "foo" should
|
|
// contain "bar".
|
|
assert_eq!(
|
|
guest.ssh_command("cat mount_image/foo").unwrap().trim(),
|
|
"bar"
|
|
);
|
|
|
|
// ACPI feature is needed.
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
guest.enable_memory_hotplug();
|
|
|
|
// Add RAM to the VM
|
|
let desired_ram = 1024 << 20;
|
|
resize_command(&api_socket, None, Some(desired_ram), None, None);
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 960_000);
|
|
|
|
// Check again the content of the block device after the resize
|
|
// has been performed.
|
|
assert_eq!(
|
|
guest.ssh_command("cat mount_image/foo").unwrap().trim(),
|
|
"bar"
|
|
);
|
|
}
|
|
|
|
// Unmount the device
|
|
guest.ssh_command("sudo umount /dev/vdc").unwrap();
|
|
guest.ssh_command("rm -r mount_image").unwrap();
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
if let Some(mut daemon_child) = daemon_child {
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
let _ = daemon_child.kill();
|
|
let _ = daemon_child.wait();
|
|
}
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
fn test_boot_from_vhost_user_blk(
|
|
num_queues: usize,
|
|
readonly: bool,
|
|
direct: bool,
|
|
prepare_vhost_user_blk_daemon: Option<&PrepareBlkDaemon>,
|
|
) {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
let disk_path = guest.disk_config.disk(DiskType::OperatingSystem).unwrap();
|
|
|
|
let (blk_boot_params, daemon_child) = {
|
|
let prepare_daemon = prepare_vhost_user_blk_daemon.unwrap();
|
|
// Start the daemon
|
|
let (daemon_child, vubd_socket_path) = prepare_daemon(
|
|
&guest.tmp_dir,
|
|
disk_path.as_str(),
|
|
num_queues,
|
|
readonly,
|
|
direct,
|
|
);
|
|
|
|
(
|
|
format!(
|
|
"vhost_user=true,socket={vubd_socket_path},num_queues={num_queues},queue_size=128",
|
|
),
|
|
Some(daemon_child),
|
|
)
|
|
};
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", format!("boot={num_queues}").as_str()])
|
|
.args(["--memory", "size=512M,shared=on"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.args([
|
|
"--disk",
|
|
blk_boot_params.as_str(),
|
|
format!(
|
|
"path={}",
|
|
guest.disk_config.disk(DiskType::CloudInit).unwrap()
|
|
)
|
|
.as_str(),
|
|
])
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Just check the VM booted correctly.
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default(), num_queues as u32);
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
|
});
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
if let Some(mut daemon_child) = daemon_child {
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
let _ = daemon_child.kill();
|
|
let _ = daemon_child.wait();
|
|
}
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
fn _test_virtio_fs(
|
|
prepare_daemon: &dyn Fn(&TempDir, &str) -> (std::process::Child, String),
|
|
hotplug: bool,
|
|
pci_segment: Option<u16>,
|
|
) {
|
|
#[cfg(target_arch = "aarch64")]
|
|
let focal_image = if hotplug {
|
|
FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string()
|
|
} else {
|
|
FOCAL_IMAGE_NAME.to_string()
|
|
};
|
|
#[cfg(target_arch = "x86_64")]
|
|
let focal_image = FOCAL_IMAGE_NAME.to_string();
|
|
let focal = UbuntuDiskConfig::new(focal_image);
|
|
let guest = Guest::new(Box::new(focal));
|
|
let api_socket = 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");
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
let kernel_path = direct_kernel_boot_path();
|
|
#[cfg(target_arch = "aarch64")]
|
|
let kernel_path = if hotplug {
|
|
edk2_path()
|
|
} else {
|
|
direct_kernel_boot_path()
|
|
};
|
|
|
|
let (mut daemon_child, virtiofsd_socket_path) =
|
|
prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap());
|
|
|
|
let mut guest_command = GuestCommand::new(&guest);
|
|
guest_command
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M,hotplug_size=2048M,shared=on"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.default_net()
|
|
.args(["--api-socket", &api_socket]);
|
|
if pci_segment.is_some() {
|
|
guest_command.args([
|
|
"--platform",
|
|
&format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"),
|
|
]);
|
|
}
|
|
|
|
let fs_params = format!(
|
|
"id=myfs0,tag=myfs,socket={},num_queues=1,queue_size=1024{}",
|
|
virtiofsd_socket_path,
|
|
if let Some(pci_segment) = pci_segment {
|
|
format!(",pci_segment={pci_segment}")
|
|
} else {
|
|
"".to_owned()
|
|
}
|
|
);
|
|
|
|
if !hotplug {
|
|
guest_command.args(["--fs", fs_params.as_str()]);
|
|
}
|
|
|
|
let mut child = guest_command.capture_output().spawn().unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
if hotplug {
|
|
// Add fs to the VM
|
|
let (cmd_success, cmd_output) =
|
|
remote_command_w_output(&api_socket, "add-fs", Some(&fs_params));
|
|
assert!(cmd_success);
|
|
|
|
if let Some(pci_segment) = pci_segment {
|
|
assert!(String::from_utf8_lossy(&cmd_output).contains(&format!(
|
|
"{{\"id\":\"myfs0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}"
|
|
)));
|
|
} else {
|
|
assert!(String::from_utf8_lossy(&cmd_output)
|
|
.contains("{\"id\":\"myfs0\",\"bdf\":\"0000:00:06.0\"}"));
|
|
}
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
}
|
|
|
|
// Mount shared directory through virtio_fs filesystem
|
|
guest
|
|
.ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/")
|
|
.unwrap();
|
|
|
|
// Check file1 exists and its content is "foo"
|
|
assert_eq!(
|
|
guest.ssh_command("cat mount_dir/file1").unwrap().trim(),
|
|
"foo"
|
|
);
|
|
// Check file2 does not exist
|
|
guest
|
|
.ssh_command("[ ! -f 'mount_dir/file2' ] || true")
|
|
.unwrap();
|
|
|
|
// Check file3 exists and its content is "bar"
|
|
assert_eq!(
|
|
guest.ssh_command("cat mount_dir/file3").unwrap().trim(),
|
|
"bar"
|
|
);
|
|
|
|
// ACPI feature is needed.
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
guest.enable_memory_hotplug();
|
|
|
|
// Add RAM to the VM
|
|
let desired_ram = 1024 << 20;
|
|
resize_command(&api_socket, None, Some(desired_ram), None, None);
|
|
|
|
thread::sleep(std::time::Duration::new(30, 0));
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 960_000);
|
|
|
|
// After the resize, check again that file1 exists and its
|
|
// content is "foo".
|
|
assert_eq!(
|
|
guest.ssh_command("cat mount_dir/file1").unwrap().trim(),
|
|
"foo"
|
|
);
|
|
}
|
|
|
|
if hotplug {
|
|
// Remove from VM
|
|
guest.ssh_command("sudo umount mount_dir").unwrap();
|
|
assert!(remote_command(&api_socket, "remove-device", Some("myfs0")));
|
|
}
|
|
});
|
|
|
|
let (r, hotplug_daemon_child) = if r.is_ok() && hotplug {
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
let (daemon_child, virtiofsd_socket_path) =
|
|
prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap());
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
let fs_params = format!(
|
|
"id=myfs0,tag=myfs,socket={},num_queues=1,queue_size=1024{}",
|
|
virtiofsd_socket_path,
|
|
if let Some(pci_segment) = pci_segment {
|
|
format!(",pci_segment={pci_segment}")
|
|
} else {
|
|
"".to_owned()
|
|
}
|
|
);
|
|
|
|
// Add back and check it works
|
|
let (cmd_success, cmd_output) =
|
|
remote_command_w_output(&api_socket, "add-fs", Some(&fs_params));
|
|
assert!(cmd_success);
|
|
if let Some(pci_segment) = pci_segment {
|
|
assert!(String::from_utf8_lossy(&cmd_output).contains(&format!(
|
|
"{{\"id\":\"myfs0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}"
|
|
)));
|
|
} else {
|
|
assert!(String::from_utf8_lossy(&cmd_output)
|
|
.contains("{\"id\":\"myfs0\",\"bdf\":\"0000:00:06.0\"}"));
|
|
}
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
// Mount shared directory through virtio_fs filesystem
|
|
guest
|
|
.ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/")
|
|
.unwrap();
|
|
|
|
// Check file1 exists and its content is "foo"
|
|
assert_eq!(
|
|
guest.ssh_command("cat mount_dir/file1").unwrap().trim(),
|
|
"foo"
|
|
);
|
|
});
|
|
|
|
(r, Some(daemon_child))
|
|
} else {
|
|
(r, None)
|
|
};
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
let _ = daemon_child.kill();
|
|
let _ = daemon_child.wait();
|
|
|
|
if let Some(mut daemon_child) = hotplug_daemon_child {
|
|
let _ = daemon_child.kill();
|
|
let _ = daemon_child.wait();
|
|
}
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
fn test_virtio_pmem(discard_writes: bool, specify_size: bool) {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
let pmem_temp_file = TempFile::new().unwrap();
|
|
pmem_temp_file.as_file().set_len(128 << 20).unwrap();
|
|
|
|
std::process::Command::new("mkfs.ext4")
|
|
.arg(pmem_temp_file.as_path())
|
|
.output()
|
|
.expect("Expect creating disk image to succeed");
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.default_net()
|
|
.args([
|
|
"--pmem",
|
|
format!(
|
|
"file={}{}{}",
|
|
pmem_temp_file.as_path().to_str().unwrap(),
|
|
if specify_size { ",size=128M" } else { "" },
|
|
if discard_writes {
|
|
",discard_writes=on"
|
|
} else {
|
|
""
|
|
}
|
|
)
|
|
.as_str(),
|
|
])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Check for the presence of /dev/pmem0
|
|
assert_eq!(
|
|
guest.ssh_command("ls /dev/pmem0").unwrap().trim(),
|
|
"/dev/pmem0"
|
|
);
|
|
|
|
// Check changes persist after reboot
|
|
assert_eq!(guest.ssh_command("sudo mount /dev/pmem0 /mnt").unwrap(), "");
|
|
assert_eq!(guest.ssh_command("ls /mnt").unwrap(), "lost+found\n");
|
|
guest
|
|
.ssh_command("echo test123 | sudo tee /mnt/test")
|
|
.unwrap();
|
|
assert_eq!(guest.ssh_command("sudo umount /mnt").unwrap(), "");
|
|
assert_eq!(guest.ssh_command("ls /mnt").unwrap(), "");
|
|
|
|
guest.reboot_linux(0, None);
|
|
assert_eq!(guest.ssh_command("sudo mount /dev/pmem0 /mnt").unwrap(), "");
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("sudo cat /mnt/test || true")
|
|
.unwrap()
|
|
.trim(),
|
|
if discard_writes { "" } else { "test123" }
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
fn get_fd_count(pid: u32) -> usize {
|
|
fs::read_dir(format!("/proc/{pid}/fd")).unwrap().count()
|
|
}
|
|
|
|
fn _test_virtio_vsock(hotplug: bool) {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
let kernel_path = direct_kernel_boot_path();
|
|
#[cfg(target_arch = "aarch64")]
|
|
let kernel_path = if hotplug {
|
|
edk2_path()
|
|
} else {
|
|
direct_kernel_boot_path()
|
|
};
|
|
|
|
let socket = temp_vsock_path(&guest.tmp_dir);
|
|
let api_socket = temp_api_path(&guest.tmp_dir);
|
|
|
|
let mut cmd = GuestCommand::new(&guest);
|
|
cmd.args(["--api-socket", &api_socket]);
|
|
cmd.args(["--cpus", "boot=1"]);
|
|
cmd.args(["--memory", "size=512M"]);
|
|
cmd.args(["--kernel", kernel_path.to_str().unwrap()]);
|
|
cmd.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]);
|
|
cmd.default_disks();
|
|
cmd.default_net();
|
|
|
|
if !hotplug {
|
|
cmd.args(["--vsock", format!("cid=3,socket={socket}").as_str()]);
|
|
}
|
|
|
|
let mut child = cmd.capture_output().spawn().unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
if hotplug {
|
|
let (cmd_success, cmd_output) = remote_command_w_output(
|
|
&api_socket,
|
|
"add-vsock",
|
|
Some(format!("cid=3,socket={socket},id=test0").as_str()),
|
|
);
|
|
assert!(cmd_success);
|
|
assert!(String::from_utf8_lossy(&cmd_output)
|
|
.contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}"));
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
// Check adding a second one fails
|
|
assert!(!remote_command(
|
|
&api_socket,
|
|
"add-vsock",
|
|
Some("cid=1234,socket=/tmp/fail")
|
|
));
|
|
}
|
|
|
|
// Validate vsock works as expected.
|
|
guest.check_vsock(socket.as_str());
|
|
guest.reboot_linux(0, None);
|
|
// Validate vsock still works after a reboot.
|
|
guest.check_vsock(socket.as_str());
|
|
|
|
if hotplug {
|
|
assert!(remote_command(&api_socket, "remove-device", Some("test0")));
|
|
}
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
fn get_ksm_pages_shared() -> u32 {
|
|
fs::read_to_string("/sys/kernel/mm/ksm/pages_shared")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap()
|
|
}
|
|
|
|
fn test_memory_mergeable(mergeable: bool) {
|
|
let memory_param = if mergeable {
|
|
"mergeable=on"
|
|
} else {
|
|
"mergeable=off"
|
|
};
|
|
|
|
// We are assuming the rest of the system in our CI is not using mergeable memory
|
|
let ksm_ps_init = get_ksm_pages_shared();
|
|
assert!(ksm_ps_init == 0);
|
|
|
|
let focal1 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest1 = Guest::new(Box::new(focal1));
|
|
let mut child1 = GuestCommand::new(&guest1)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", format!("size=512M,{memory_param}").as_str()])
|
|
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.args(["--net", guest1.default_net_string().as_str()])
|
|
.args(["--serial", "tty", "--console", "off"])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest1.wait_vm_boot(None).unwrap();
|
|
});
|
|
if r.is_err() {
|
|
kill_child(&mut child1);
|
|
let output = child1.wait_with_output().unwrap();
|
|
handle_child_output(r, &output);
|
|
panic!("Test should already be failed/panicked"); // To explicitly mark this block never return
|
|
}
|
|
|
|
let ksm_ps_guest1 = get_ksm_pages_shared();
|
|
|
|
let focal2 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest2 = Guest::new(Box::new(focal2));
|
|
let mut child2 = GuestCommand::new(&guest2)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", format!("size=512M,{memory_param}").as_str()])
|
|
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.args(["--net", guest2.default_net_string().as_str()])
|
|
.args(["--serial", "tty", "--console", "off"])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest2.wait_vm_boot(None).unwrap();
|
|
let ksm_ps_guest2 = get_ksm_pages_shared();
|
|
|
|
if mergeable {
|
|
println!(
|
|
"ksm pages_shared after vm1 booted '{ksm_ps_guest1}', ksm pages_shared after vm2 booted '{ksm_ps_guest2}'"
|
|
);
|
|
// We are expecting the number of shared pages to increase as the number of VM increases
|
|
assert!(ksm_ps_guest1 < ksm_ps_guest2);
|
|
} else {
|
|
assert!(ksm_ps_guest1 == 0);
|
|
assert!(ksm_ps_guest2 == 0);
|
|
}
|
|
});
|
|
|
|
kill_child(&mut child1);
|
|
kill_child(&mut child2);
|
|
|
|
let output = child1.wait_with_output().unwrap();
|
|
child2.wait().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
fn _get_vmm_overhead(pid: u32, guest_memory_size: u32) -> HashMap<String, u32> {
|
|
let smaps = fs::File::open(format!("/proc/{pid}/smaps")).unwrap();
|
|
let reader = io::BufReader::new(smaps);
|
|
|
|
let mut skip_map: bool = false;
|
|
let mut region_name: String = "".to_string();
|
|
let mut region_maps = HashMap::new();
|
|
for line in reader.lines() {
|
|
let l = line.unwrap();
|
|
|
|
if l.contains('-') {
|
|
let values: Vec<&str> = l.split_whitespace().collect();
|
|
region_name = values.last().unwrap().trim().to_string();
|
|
if region_name == "0" {
|
|
region_name = "anonymous".to_string()
|
|
}
|
|
}
|
|
|
|
// Each section begins with something that looks like:
|
|
// Size: 2184 kB
|
|
if l.starts_with("Size:") {
|
|
let values: Vec<&str> = l.split_whitespace().collect();
|
|
let map_size = values[1].parse::<u32>().unwrap();
|
|
// We skip the assigned guest RAM map, its RSS is only
|
|
// dependent on the guest actual memory usage.
|
|
// Everything else can be added to the VMM overhead.
|
|
skip_map = map_size >= guest_memory_size;
|
|
continue;
|
|
}
|
|
|
|
// If this is a map we're taking into account, then we only
|
|
// count the RSS. The sum of all counted RSS is the VMM overhead.
|
|
if !skip_map && l.starts_with("Rss:") {
|
|
let values: Vec<&str> = l.split_whitespace().collect();
|
|
let value = values[1].trim().parse::<u32>().unwrap();
|
|
*region_maps.entry(region_name.clone()).or_insert(0) += value;
|
|
}
|
|
}
|
|
|
|
region_maps
|
|
}
|
|
|
|
fn get_vmm_overhead(pid: u32, guest_memory_size: u32) -> u32 {
|
|
let mut total = 0;
|
|
|
|
for (region_name, value) in &_get_vmm_overhead(pid, guest_memory_size) {
|
|
eprintln!("{region_name}: {value}");
|
|
total += value;
|
|
}
|
|
|
|
total
|
|
}
|
|
|
|
fn process_rss_kib(pid: u32) -> usize {
|
|
let command = format!("ps -q {pid} -o rss=");
|
|
let rss = exec_host_command_output(&command);
|
|
String::from_utf8_lossy(&rss.stdout).trim().parse().unwrap()
|
|
}
|
|
|
|
// 10MB is our maximum accepted overhead.
|
|
const MAXIMUM_VMM_OVERHEAD_KB: u32 = 10 * 1024;
|
|
|
|
#[derive(PartialEq, Eq, PartialOrd)]
|
|
struct Counters {
|
|
rx_bytes: u64,
|
|
rx_frames: u64,
|
|
tx_bytes: u64,
|
|
tx_frames: u64,
|
|
read_bytes: u64,
|
|
write_bytes: u64,
|
|
read_ops: u64,
|
|
write_ops: u64,
|
|
}
|
|
|
|
fn get_counters(api_socket: &str) -> Counters {
|
|
// Get counters
|
|
let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "counters", None);
|
|
assert!(cmd_success);
|
|
|
|
let counters: HashMap<&str, HashMap<&str, u64>> =
|
|
serde_json::from_slice(&cmd_output).unwrap_or_default();
|
|
|
|
let rx_bytes = *counters.get("_net2").unwrap().get("rx_bytes").unwrap();
|
|
let rx_frames = *counters.get("_net2").unwrap().get("rx_frames").unwrap();
|
|
let tx_bytes = *counters.get("_net2").unwrap().get("tx_bytes").unwrap();
|
|
let tx_frames = *counters.get("_net2").unwrap().get("tx_frames").unwrap();
|
|
|
|
let read_bytes = *counters.get("_disk0").unwrap().get("read_bytes").unwrap();
|
|
let write_bytes = *counters.get("_disk0").unwrap().get("write_bytes").unwrap();
|
|
let read_ops = *counters.get("_disk0").unwrap().get("read_ops").unwrap();
|
|
let write_ops = *counters.get("_disk0").unwrap().get("write_ops").unwrap();
|
|
|
|
Counters {
|
|
rx_bytes,
|
|
rx_frames,
|
|
tx_bytes,
|
|
tx_frames,
|
|
read_bytes,
|
|
write_bytes,
|
|
read_ops,
|
|
write_ops,
|
|
}
|
|
}
|
|
|
|
fn pty_read(mut pty: std::fs::File) -> Receiver<String> {
|
|
let (tx, rx) = mpsc::channel::<String>();
|
|
thread::spawn(move || loop {
|
|
thread::sleep(std::time::Duration::new(1, 0));
|
|
let mut buf = [0; 512];
|
|
match pty.read(&mut buf) {
|
|
Ok(_bytes) => {
|
|
let output = std::str::from_utf8(&buf).unwrap().to_string();
|
|
match tx.send(output) {
|
|
Ok(_) => (),
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
Err(_) => break,
|
|
}
|
|
});
|
|
rx
|
|
}
|
|
|
|
fn get_pty_path(api_socket: &str, pty_type: &str) -> PathBuf {
|
|
let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "info", None);
|
|
assert!(cmd_success);
|
|
let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default();
|
|
assert_eq!("Pty", info["config"][pty_type]["mode"]);
|
|
PathBuf::from(
|
|
info["config"][pty_type]["file"]
|
|
.as_str()
|
|
.expect("Missing pty path"),
|
|
)
|
|
}
|
|
|
|
// VFIO test network setup.
|
|
// We reserve a different IP class for it: 172.18.0.0/24.
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn setup_vfio_network_interfaces() {
|
|
// 'vfio-br0'
|
|
assert!(exec_host_command_status("sudo ip link add name vfio-br0 type bridge").success());
|
|
assert!(exec_host_command_status("sudo ip link set vfio-br0 up").success());
|
|
assert!(exec_host_command_status("sudo ip addr add 172.18.0.1/24 dev vfio-br0").success());
|
|
// 'vfio-tap0'
|
|
assert!(exec_host_command_status("sudo ip tuntap add vfio-tap0 mode tap").success());
|
|
assert!(exec_host_command_status("sudo ip link set vfio-tap0 master vfio-br0").success());
|
|
assert!(exec_host_command_status("sudo ip link set vfio-tap0 up").success());
|
|
// 'vfio-tap1'
|
|
assert!(exec_host_command_status("sudo ip tuntap add vfio-tap1 mode tap").success());
|
|
assert!(exec_host_command_status("sudo ip link set vfio-tap1 master vfio-br0").success());
|
|
assert!(exec_host_command_status("sudo ip link set vfio-tap1 up").success());
|
|
// 'vfio-tap2'
|
|
assert!(exec_host_command_status("sudo ip tuntap add vfio-tap2 mode tap").success());
|
|
assert!(exec_host_command_status("sudo ip link set vfio-tap2 master vfio-br0").success());
|
|
assert!(exec_host_command_status("sudo ip link set vfio-tap2 up").success());
|
|
// 'vfio-tap3'
|
|
assert!(exec_host_command_status("sudo ip tuntap add vfio-tap3 mode tap").success());
|
|
assert!(exec_host_command_status("sudo ip link set vfio-tap3 master vfio-br0").success());
|
|
assert!(exec_host_command_status("sudo ip link set vfio-tap3 up").success());
|
|
}
|
|
|
|
// Tear VFIO test network down
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn cleanup_vfio_network_interfaces() {
|
|
assert!(exec_host_command_status("sudo ip link del vfio-br0").success());
|
|
assert!(exec_host_command_status("sudo ip link del vfio-tap0").success());
|
|
assert!(exec_host_command_status("sudo ip link del vfio-tap1").success());
|
|
assert!(exec_host_command_status("sudo ip link del vfio-tap2").success());
|
|
assert!(exec_host_command_status("sudo ip link del vfio-tap3").success());
|
|
}
|
|
|
|
fn balloon_size(api_socket: &str) -> u64 {
|
|
let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "info", None);
|
|
assert!(cmd_success);
|
|
|
|
let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default();
|
|
let total_mem = &info["config"]["memory"]["size"]
|
|
.to_string()
|
|
.parse::<u64>()
|
|
.unwrap();
|
|
let actual_mem = &info["memory_actual_size"]
|
|
.to_string()
|
|
.parse::<u64>()
|
|
.unwrap();
|
|
total_mem - actual_mem
|
|
}
|
|
|
|
fn vm_state(api_socket: &str) -> String {
|
|
let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "info", None);
|
|
assert!(cmd_success);
|
|
|
|
let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default();
|
|
let state = &info["state"].as_str().unwrap();
|
|
|
|
state.to_string()
|
|
}
|
|
|
|
// This test validates that it can find the virtio-iommu device at first.
|
|
// It also verifies that both disks and the network card are attached to
|
|
// the virtual IOMMU by looking at /sys/kernel/iommu_groups directory.
|
|
// The last interesting part of this test is that it exercises the network
|
|
// interface attached to the virtual IOMMU since this is the one used to
|
|
// send all commands through SSH.
|
|
fn _test_virtio_iommu(acpi: bool) {
|
|
// Virtio-iommu support is ready in recent kernel (v5.14). But the kernel in
|
|
// Focal image is still old.
|
|
// So if ACPI is enabled on AArch64, we use a modified Focal image in which
|
|
// the kernel binary has been updated.
|
|
#[cfg(target_arch = "aarch64")]
|
|
let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string();
|
|
#[cfg(target_arch = "x86_64")]
|
|
let focal_image = FOCAL_IMAGE_NAME.to_string();
|
|
let focal = UbuntuDiskConfig::new(focal_image);
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
let kernel_path = direct_kernel_boot_path();
|
|
#[cfg(target_arch = "aarch64")]
|
|
let kernel_path = if acpi {
|
|
edk2_path()
|
|
} else {
|
|
direct_kernel_boot_path()
|
|
};
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.args([
|
|
"--disk",
|
|
format!(
|
|
"path={},iommu=on",
|
|
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
|
|
)
|
|
.as_str(),
|
|
format!(
|
|
"path={},iommu=on",
|
|
guest.disk_config.disk(DiskType::CloudInit).unwrap()
|
|
)
|
|
.as_str(),
|
|
])
|
|
.args(["--net", guest.default_net_string_w_iommu().as_str()])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Verify the virtio-iommu device is present.
|
|
assert!(guest
|
|
.does_device_vendor_pair_match("0x1057", "0x1af4")
|
|
.unwrap_or_default());
|
|
|
|
// On AArch64, if the guest system boots from FDT, the behavior of IOMMU is a bit
|
|
// different with ACPI.
|
|
// All devices on the PCI bus will be attached to the virtual IOMMU, except the
|
|
// virtio-iommu device itself. So these devices will all be added to IOMMU groups,
|
|
// and appear under folder '/sys/kernel/iommu_groups/'.
|
|
// The result is, in the case of FDT, IOMMU group '0' contains "0000:00:01.0"
|
|
// which is the console. The first disk "0000:00:02.0" is in group '1'.
|
|
// While on ACPI, console device is not attached to IOMMU. So the IOMMU group '0'
|
|
// contains "0000:00:02.0" which is the first disk.
|
|
//
|
|
// Verify the iommu group of the first disk.
|
|
let iommu_group = !acpi as i32;
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(format!("ls /sys/kernel/iommu_groups/{iommu_group}/devices").as_str())
|
|
.unwrap()
|
|
.trim(),
|
|
"0000:00:02.0"
|
|
);
|
|
|
|
// Verify the iommu group of the second disk.
|
|
let iommu_group = if acpi { 1 } else { 2 };
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(format!("ls /sys/kernel/iommu_groups/{iommu_group}/devices").as_str())
|
|
.unwrap()
|
|
.trim(),
|
|
"0000:00:03.0"
|
|
);
|
|
|
|
// Verify the iommu group of the network card.
|
|
let iommu_group = if acpi { 2 } else { 3 };
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(format!("ls /sys/kernel/iommu_groups/{iommu_group}/devices").as_str())
|
|
.unwrap()
|
|
.trim(),
|
|
"0000:00:04.0"
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
fn get_reboot_count(guest: &Guest) -> u32 {
|
|
guest
|
|
.ssh_command("sudo last | grep -c reboot")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn enable_guest_watchdog(guest: &Guest, watchdog_sec: u32) {
|
|
// Check for PCI device
|
|
assert!(guest
|
|
.does_device_vendor_pair_match("0x1063", "0x1af4")
|
|
.unwrap_or_default());
|
|
|
|
// Enable systemd watchdog
|
|
guest
|
|
.ssh_command(&format!(
|
|
"echo RuntimeWatchdogSec={watchdog_sec}s | sudo tee -a /etc/systemd/system.conf"
|
|
))
|
|
.unwrap();
|
|
|
|
guest.ssh_command("sudo systemctl daemon-reexec").unwrap();
|
|
}
|
|
|
|
fn make_guest_panic(guest: &Guest) {
|
|
// Check for pvpanic device
|
|
assert!(guest
|
|
.does_device_vendor_pair_match("0x0011", "0x1b36")
|
|
.unwrap_or_default());
|
|
|
|
// Trigger guest a panic
|
|
guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap();
|
|
}
|
|
|
|
mod common_parallel {
|
|
use std::{fs::OpenOptions, io::SeekFrom};
|
|
|
|
use crate::*;
|
|
|
|
#[test]
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_focal_hypervisor_fw() {
|
|
test_simple_launch(fw_path(FwType::RustHypervisorFirmware), FOCAL_IMAGE_NAME)
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_focal_ovmf() {
|
|
test_simple_launch(fw_path(FwType::Ovmf), FOCAL_IMAGE_NAME)
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_simple_launch(fw_path: String, disk_path: &str) {
|
|
let disk_config = Box::new(UbuntuDiskConfig::new(disk_path.to_string()));
|
|
let guest = Guest::new(disk_config);
|
|
let event_path = temp_event_monitor_path(&guest.tmp_dir);
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", fw_path.as_str()])
|
|
.default_disks()
|
|
.default_net()
|
|
.args(["--serial", "tty", "--console", "off"])
|
|
.args(["--event-monitor", format!("path={event_path}").as_str()])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(Some(120)).unwrap();
|
|
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1);
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
|
assert_eq!(guest.get_pci_bridge_class().unwrap_or_default(), "0x060000");
|
|
|
|
let expected_sequential_events = [
|
|
&MetaEvent {
|
|
event: "starting".to_string(),
|
|
device_id: None,
|
|
},
|
|
&MetaEvent {
|
|
event: "booting".to_string(),
|
|
device_id: None,
|
|
},
|
|
&MetaEvent {
|
|
event: "booted".to_string(),
|
|
device_id: None,
|
|
},
|
|
&MetaEvent {
|
|
event: "activated".to_string(),
|
|
device_id: Some("_disk0".to_string()),
|
|
},
|
|
&MetaEvent {
|
|
event: "reset".to_string(),
|
|
device_id: Some("_disk0".to_string()),
|
|
},
|
|
];
|
|
assert!(check_sequential_events(
|
|
&expected_sequential_events,
|
|
&event_path
|
|
));
|
|
|
|
// It's been observed on the Bionic image that udev and snapd
|
|
// services can cause some delay in the VM's shutdown. Disabling
|
|
// them improves the reliability of this test.
|
|
let _ = guest.ssh_command("sudo systemctl disable udev");
|
|
let _ = guest.ssh_command("sudo systemctl stop udev");
|
|
let _ = guest.ssh_command("sudo systemctl disable snapd");
|
|
let _ = guest.ssh_command("sudo systemctl stop snapd");
|
|
|
|
guest.ssh_command("sudo poweroff").unwrap();
|
|
thread::sleep(std::time::Duration::new(20, 0));
|
|
let latest_events = [
|
|
&MetaEvent {
|
|
event: "shutdown".to_string(),
|
|
device_id: None,
|
|
},
|
|
&MetaEvent {
|
|
event: "deleted".to_string(),
|
|
device_id: None,
|
|
},
|
|
&MetaEvent {
|
|
event: "shutdown".to_string(),
|
|
device_id: None,
|
|
},
|
|
];
|
|
assert!(check_latest_events_exact(&latest_events, &event_path));
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi_cpu() {
|
|
let jammy_image = JAMMY_IMAGE_NAME.to_string();
|
|
let jammy = UbuntuDiskConfig::new(jammy_image);
|
|
let guest = Guest::new(Box::new(jammy));
|
|
|
|
let mut cmd = GuestCommand::new(&guest);
|
|
cmd.args(["--cpus", "boot=2,max=4"])
|
|
.args(["--memory", "size=512M"])
|
|
.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();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(Some(120)).unwrap();
|
|
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2);
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(
|
|
r#"sudo dmesg | grep "smp: Brought up" | sed "s/\[\ *[0-9.]*\] //""#
|
|
)
|
|
.unwrap()
|
|
.trim(),
|
|
"smp: Brought up 1 node, 2 CPUs"
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let max_phys_bits: u8 = 36;
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", &format!("max_phys_bits={max_phys_bits}")])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).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);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cpu_affinity() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
// We need the host to have at least 4 CPUs if we want to be able
|
|
// to run this test.
|
|
let host_cpus_count = exec_host_command_output("nproc");
|
|
assert!(
|
|
String::from_utf8_lossy(&host_cpus_count.stdout)
|
|
.trim()
|
|
.parse::<u16>()
|
|
.unwrap_or(0)
|
|
>= 4
|
|
);
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=2,affinity=[0@[0,2],1@[1,3]]"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).unwrap();
|
|
let pid = child.id();
|
|
let taskset_vcpu0 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep vcpu0 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
|
|
assert_eq!(String::from_utf8_lossy(&taskset_vcpu0.stdout).trim(), "0,2");
|
|
let taskset_vcpu1 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep vcpu1 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
|
|
assert_eq!(String::from_utf8_lossy(&taskset_vcpu1.stdout).trim(), "1,3");
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_queue_affinity() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
// We need the host to have at least 4 CPUs if we want to be able
|
|
// to run this test.
|
|
let host_cpus_count = exec_host_command_output("nproc");
|
|
assert!(
|
|
String::from_utf8_lossy(&host_cpus_count.stdout)
|
|
.trim()
|
|
.parse::<u16>()
|
|
.unwrap_or(0)
|
|
>= 4
|
|
);
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=4"])
|
|
.args(["--memory", "size=512M"])
|
|
.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={},num_queues=4,queue_affinity=[0@[0,2],1@[1,3],2@[1],3@[3]]",
|
|
guest.disk_config.disk(DiskType::CloudInit).unwrap()
|
|
)
|
|
.as_str(),
|
|
])
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
let pid = child.id();
|
|
let taskset_q0 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q0 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
|
|
assert_eq!(String::from_utf8_lossy(&taskset_q0.stdout).trim(), "0,2");
|
|
let taskset_q1 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q1 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
|
|
assert_eq!(String::from_utf8_lossy(&taskset_q1.stdout).trim(), "1,3");
|
|
let taskset_q2 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q2 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
|
|
assert_eq!(String::from_utf8_lossy(&taskset_q2.stdout).trim(), "1");
|
|
let taskset_q3 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q3 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str());
|
|
assert_eq!(String::from_utf8_lossy(&taskset_q3.stdout).trim(), "3");
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(not(feature = "mshv"))]
|
|
fn test_large_vm() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
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(None).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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let mut cmd = GuestCommand::new(&guest);
|
|
cmd.args(["--cpus", "boot=1"])
|
|
.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(Some(120)).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() {
|
|
_test_power_button(false);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(not(feature = "mshv"))]
|
|
fn test_user_defined_memory_regions() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
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=1"])
|
|
.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(None).unwrap();
|
|
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 2_880_000);
|
|
|
|
guest.enable_memory_hotplug();
|
|
|
|
resize_zone_command(&api_socket, "mem0", "3G");
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 4_800_000);
|
|
resize_zone_command(&api_socket, "mem2", "3G");
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 6_720_000);
|
|
resize_zone_command(&api_socket, "mem0", "2G");
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000);
|
|
resize_zone_command(&api_socket, "mem2", "2G");
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 4_800_000);
|
|
|
|
guest.reboot_linux(0, None);
|
|
|
|
// 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");
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
assert!(guest.get_total_memory().unwrap_or_default() < 4_800_000);
|
|
resize_zone_command(&api_socket, "mem2", "1G");
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
assert!(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"))]
|
|
fn test_guest_numa_nodes() {
|
|
_test_guest_numa_nodes(false);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_iommu_segments() {
|
|
let focal_image = FOCAL_IMAGE_NAME.to_string();
|
|
let focal = UbuntuDiskConfig::new(focal_image);
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
// 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.args(["--cpus", "boot=1"])
|
|
.args(["--api-socket", &api_socket])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).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_eq!(
|
|
guest
|
|
.ssh_command("ls /sys/kernel/iommu_groups/0/devices")
|
|
.unwrap()
|
|
.trim(),
|
|
"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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let mut cmd = GuestCommand::new(&guest);
|
|
cmd.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).unwrap();
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
let grep_cmd = "grep -c PCI-MSI /proc/interrupts";
|
|
#[cfg(target_arch = "aarch64")]
|
|
let grep_cmd = "grep -c ITS-MSI /proc/interrupts";
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(grep_cmd)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
12
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_net_ctrl_queue() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let mut cmd = GuestCommand::new(&guest);
|
|
cmd.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.args(["--net", guest.default_net_string_w_mtu(3000).as_str()])
|
|
.capture_output()
|
|
.default_disks();
|
|
|
|
let mut child = cmd.spawn().unwrap();
|
|
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
#[cfg(target_arch = "aarch64")]
|
|
let iface = "enp0s4";
|
|
#[cfg(target_arch = "x86_64")]
|
|
let iface = "ens4";
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(
|
|
format!("sudo ethtool -K {iface} rx-gro-hw off && echo success").as_str()
|
|
)
|
|
.unwrap()
|
|
.trim(),
|
|
"success"
|
|
);
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(format!("cat /sys/class/net/{iface}/mtu").as_str())
|
|
.unwrap()
|
|
.trim(),
|
|
"3000"
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pci_multiple_segments() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
// Prepare another disk file for the virtio-disk device
|
|
let test_disk_path = String::from(
|
|
guest
|
|
.tmp_dir
|
|
.as_path()
|
|
.join("test-disk.raw")
|
|
.to_str()
|
|
.unwrap(),
|
|
);
|
|
assert!(
|
|
exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success()
|
|
);
|
|
assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success());
|
|
|
|
let mut cmd = GuestCommand::new(&guest);
|
|
cmd.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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}"),
|
|
])
|
|
.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=15").as_str(),
|
|
])
|
|
.capture_output()
|
|
.default_net();
|
|
|
|
let mut child = cmd.spawn().unwrap();
|
|
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
let grep_cmd = "lspci | grep \"Host bridge\" | wc -l";
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
// There should be MAX_NUM_PCI_SEGMENTS PCI host bridges in the guest.
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(grep_cmd)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u16>()
|
|
.unwrap_or_default(),
|
|
MAX_NUM_PCI_SEGMENTS
|
|
);
|
|
|
|
// Check both if /dev/vdc exists and if the block size is 4M.
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep vdc | grep -c 4M")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
1
|
|
);
|
|
|
|
// Mount the device.
|
|
guest.ssh_command("mkdir mount_image").unwrap();
|
|
guest
|
|
.ssh_command("sudo mount -o rw -t ext4 /dev/vdc mount_image/")
|
|
.unwrap();
|
|
// Grant all users with write permission.
|
|
guest.ssh_command("sudo chmod a+w mount_image/").unwrap();
|
|
|
|
// Write something to the device.
|
|
guest
|
|
.ssh_command("sudo echo \"bar\" >> mount_image/foo")
|
|
.unwrap();
|
|
|
|
// Check the content of the block device. The file "foo" should
|
|
// contain "bar".
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("sudo cat mount_image/foo")
|
|
.unwrap()
|
|
.trim(),
|
|
"bar"
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pci_multiple_segments_numa_node() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
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(None).unwrap();
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(cmd)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u16>()
|
|
.unwrap_or_default(),
|
|
TEST_DISK_NODE
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_direct_kernel_boot() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
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])
|
|
.default_disks()
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1);
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
|
|
|
let grep_cmd = if cfg!(target_arch = "x86_64") {
|
|
"grep -c PCI-MSI /proc/interrupts"
|
|
} else {
|
|
"grep -c ITS-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]
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_direct_kernel_boot_bzimage() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let mut kernel_path = direct_kernel_boot_path();
|
|
// Replace the default kernel with the bzImage.
|
|
kernel_path.pop();
|
|
kernel_path.push("bzImage");
|
|
|
|
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])
|
|
.default_disks()
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1);
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
|
|
|
let grep_cmd = if cfg!(target_arch = "x86_64") {
|
|
"grep -c PCI-MSI /proc/interrupts"
|
|
} else {
|
|
"grep -c ITS-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);
|
|
}
|
|
|
|
fn _test_virtio_block(image_name: &str, disable_io_uring: bool, disable_aio: bool) {
|
|
let focal = UbuntuDiskConfig::new(image_name.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut blk_file_path = workload_path;
|
|
blk_file_path.push("blk.img");
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
let mut cloud_child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=4"])
|
|
.args(["--memory", "size=512M,shared=on"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.args([
|
|
"--disk",
|
|
format!(
|
|
"path={}",
|
|
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
|
|
)
|
|
.as_str(),
|
|
format!(
|
|
"path={}",
|
|
guest.disk_config.disk(DiskType::CloudInit).unwrap()
|
|
)
|
|
.as_str(),
|
|
format!(
|
|
"path={},readonly=on,direct=on,num_queues=4,_disable_io_uring={},_disable_aio={}",
|
|
blk_file_path.to_str().unwrap(),
|
|
disable_io_uring,
|
|
disable_aio,
|
|
)
|
|
.as_str(),
|
|
])
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Check both if /dev/vdc exists and if the block size is 16M.
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep vdc | grep -c 16M")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
1
|
|
);
|
|
|
|
// Check both if /dev/vdc exists and if this block is RO.
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep vdc | awk '{print $5}'")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
1
|
|
);
|
|
|
|
// Check if the number of queues is 4.
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
4
|
|
);
|
|
});
|
|
|
|
let _ = cloud_child.kill();
|
|
let output = cloud_child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_block_io_uring() {
|
|
_test_virtio_block(FOCAL_IMAGE_NAME, false, true)
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_block_aio() {
|
|
_test_virtio_block(FOCAL_IMAGE_NAME, true, false)
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_block_sync() {
|
|
_test_virtio_block(FOCAL_IMAGE_NAME, true, true)
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_block_qcow2() {
|
|
_test_virtio_block(FOCAL_IMAGE_NAME_QCOW2, false, false)
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_block_qcow2_backing_file() {
|
|
_test_virtio_block(FOCAL_IMAGE_NAME_QCOW2_BACKING_FILE, false, false)
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_block_vhd() {
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut raw_file_path = workload_path.clone();
|
|
let mut vhd_file_path = workload_path;
|
|
raw_file_path.push(FOCAL_IMAGE_NAME);
|
|
vhd_file_path.push(FOCAL_IMAGE_NAME_VHD);
|
|
|
|
// Generate VHD file from RAW file
|
|
std::process::Command::new("qemu-img")
|
|
.arg("convert")
|
|
.arg("-p")
|
|
.args(["-f", "raw"])
|
|
.args(["-O", "vpc"])
|
|
.args(["-o", "subformat=fixed"])
|
|
.arg(raw_file_path.to_str().unwrap())
|
|
.arg(vhd_file_path.to_str().unwrap())
|
|
.output()
|
|
.expect("Expect generating VHD image from RAW image");
|
|
|
|
_test_virtio_block(FOCAL_IMAGE_NAME_VHD, false, false)
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_block_vhdx() {
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut raw_file_path = workload_path.clone();
|
|
let mut vhdx_file_path = workload_path;
|
|
raw_file_path.push(FOCAL_IMAGE_NAME);
|
|
vhdx_file_path.push(FOCAL_IMAGE_NAME_VHDX);
|
|
|
|
// Generate dynamic VHDX file from RAW file
|
|
std::process::Command::new("qemu-img")
|
|
.arg("convert")
|
|
.arg("-p")
|
|
.args(["-f", "raw"])
|
|
.args(["-O", "vhdx"])
|
|
.arg(raw_file_path.to_str().unwrap())
|
|
.arg(vhdx_file_path.to_str().unwrap())
|
|
.output()
|
|
.expect("Expect generating dynamic VHDx image from RAW image");
|
|
|
|
_test_virtio_block(FOCAL_IMAGE_NAME_VHDX, false, false)
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_block_dynamic_vhdx_expand() {
|
|
const VIRTUAL_DISK_SIZE: u64 = 100 << 20;
|
|
const EMPTY_VHDX_FILE_SIZE: u64 = 8 << 20;
|
|
const FULL_VHDX_FILE_SIZE: u64 = 112 << 20;
|
|
const DYNAMIC_VHDX_NAME: &str = "dynamic.vhdx";
|
|
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut vhdx_file_path = workload_path;
|
|
vhdx_file_path.push(DYNAMIC_VHDX_NAME);
|
|
let vhdx_path = vhdx_file_path.to_str().unwrap();
|
|
|
|
// Generate a 100 MiB dynamic VHDX file
|
|
std::process::Command::new("qemu-img")
|
|
.arg("create")
|
|
.args(["-f", "vhdx"])
|
|
.arg(vhdx_path)
|
|
.arg(VIRTUAL_DISK_SIZE.to_string())
|
|
.output()
|
|
.expect("Expect generating dynamic VHDx image from RAW image");
|
|
|
|
// Check if the size matches with empty VHDx file size
|
|
assert_eq!(vhdx_image_size(vhdx_path), EMPTY_VHDX_FILE_SIZE);
|
|
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
let mut cloud_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={vhdx_path}").as_str(),
|
|
])
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Check both if /dev/vdc exists and if the block size is 100 MiB.
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep vdc | grep -c 100M")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
1
|
|
);
|
|
|
|
// Write 100 MB of data to the VHDx disk
|
|
guest
|
|
.ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=100")
|
|
.unwrap();
|
|
});
|
|
|
|
// Check if the size matches with expected expanded VHDx file size
|
|
assert_eq!(vhdx_image_size(vhdx_path), FULL_VHDX_FILE_SIZE);
|
|
|
|
kill_child(&mut cloud_child);
|
|
let output = cloud_child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
fn vhdx_image_size(disk_name: &str) -> u64 {
|
|
std::fs::File::open(disk_name)
|
|
.unwrap()
|
|
.seek(SeekFrom::End(0))
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_block_direct_and_firmware() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
// 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)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(Some(120)).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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).unwrap();
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("grep -c IO-APIC.*timer /proc/interrupts || true")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or(1),
|
|
0
|
|
);
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("grep -c IO-APIC.*cascade /proc/interrupts || true")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or(1),
|
|
0
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_dmi_serial_number() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.args(["--platform", "serial_number=a=b;c=d"])
|
|
.default_disks()
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("sudo cat /sys/class/dmi/id/product_serial")
|
|
.unwrap()
|
|
.trim(),
|
|
"a=b;c=d"
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_dmi_uuid() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.args(["--platform", "uuid=1e8aa28a-435d-4027-87f4-40dceff1fa0a"])
|
|
.default_disks()
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("sudo cat /sys/class/dmi/id/product_uuid")
|
|
.unwrap()
|
|
.trim(),
|
|
"1e8aa28a-435d-4027-87f4-40dceff1fa0a"
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_dmi_oem_strings() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let s1 = "io.systemd.credential:xx=yy";
|
|
let s2 = "This is a test string";
|
|
|
|
let oem_strings = format!("oem_strings=[{s1},{s2}]");
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.args(["--platform", &oem_strings])
|
|
.default_disks()
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("sudo dmidecode --oem-string count")
|
|
.unwrap()
|
|
.trim(),
|
|
"2"
|
|
);
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("sudo dmidecode --oem-string 1")
|
|
.unwrap()
|
|
.trim(),
|
|
s1
|
|
);
|
|
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("sudo dmidecode --oem-string 2")
|
|
.unwrap()
|
|
.trim(),
|
|
s2
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_fs() {
|
|
_test_virtio_fs(&prepare_virtiofsd, false, None)
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_fs_hotplug() {
|
|
_test_virtio_fs(&prepare_virtiofsd, true, None)
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(not(feature = "mshv"))]
|
|
fn test_virtio_fs_multi_segment_hotplug() {
|
|
_test_virtio_fs(&prepare_virtiofsd, true, Some(15))
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(not(feature = "mshv"))]
|
|
fn test_virtio_fs_multi_segment() {
|
|
_test_virtio_fs(&prepare_virtiofsd, false, Some(15))
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_pmem_persist_writes() {
|
|
test_virtio_pmem(false, false)
|
|
}
|
|
|
|
#[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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
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])
|
|
.default_disks()
|
|
.args([
|
|
"--net",
|
|
guest.default_net_string().as_str(),
|
|
"tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.0",
|
|
"tap=mytap1,mac=fe:1f:9e:e1:60:f2,ip=192.168.4.1,mask=255.255.255.0",
|
|
])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
let tap_count = exec_host_command_output("ip link | grep -c mytap1");
|
|
assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1");
|
|
|
|
// 3 network interfaces + default localhost ==> 4 interfaces
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("ip -o link | wc -l")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
4
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_arch = "aarch64")]
|
|
fn test_pmu_on() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.default_net()
|
|
.args(["--serial", "off"])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Test that there is no ttyS0
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command(GREP_SERIAL_IRQ_CMD)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or(1),
|
|
0
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_serial_null() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
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.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
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)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
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)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
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)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
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)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).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 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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
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])
|
|
.default_disks()
|
|
.default_net()
|
|
.args(["--console", "tty"])
|
|
.args(["--serial", "null"])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let text = String::from("On a branch floating down river a cricket, singing.");
|
|
let cmd = format!("echo {text} | sudo tee /dev/hvc0");
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
assert!(guest
|
|
.does_device_vendor_pair_match("0x1043", "0x1af4")
|
|
.unwrap_or_default());
|
|
|
|
guest.ssh_command(&cmd).unwrap();
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
handle_child_output(r, &output);
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
assert!(String::from_utf8_lossy(&output.stdout).contains(&text));
|
|
});
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_console_file() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let console_path = guest.tmp_dir.as_path().join("console-output");
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", direct_kernel_boot_path().to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.default_net()
|
|
.args([
|
|
"--console",
|
|
format!("file={}", console_path.to_str().unwrap()).as_str(),
|
|
])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
guest.ssh_command("sudo shutdown -h now").unwrap();
|
|
|
|
let _ = child.wait_timeout(std::time::Duration::from_secs(20));
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
// Check that the cloud-hypervisor binary actually terminated
|
|
assert!(output.status.success());
|
|
|
|
// Do this check after shutdown of the VM as an easy way to ensure
|
|
// all writes are flushed to disk
|
|
let mut f = std::fs::File::open(console_path).unwrap();
|
|
let mut buf = String::new();
|
|
f.read_to_string(&mut buf).unwrap();
|
|
|
|
if !buf.contains(CONSOLE_TEST_STRING) {
|
|
eprintln!(
|
|
"\n\n==== Console file output ====\n\n{buf}\n\n==== End console file output ===="
|
|
);
|
|
}
|
|
assert!(buf.contains(CONSOLE_TEST_STRING));
|
|
});
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[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 jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
|
|
let guest = Guest::new_from_ip_range(Box::new(jammy), "172.18", 0);
|
|
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
let mut vfio_path = workload_path.clone();
|
|
vfio_path.push("vfio");
|
|
|
|
let mut cloud_init_vfio_base_path = vfio_path.clone();
|
|
cloud_init_vfio_base_path.push("cloudinit.img");
|
|
|
|
// We copy our cloudinit into the vfio mount point, for the nested
|
|
// cloud-hypervisor guest to use.
|
|
rate_limited_copy(
|
|
guest.disk_config.disk(DiskType::CloudInit).unwrap(),
|
|
&cloud_init_vfio_base_path,
|
|
)
|
|
.expect("copying of cloud-init disk failed");
|
|
|
|
let mut vfio_disk_path = workload_path.clone();
|
|
vfio_disk_path.push("vfio.img");
|
|
|
|
// Create the vfio disk image
|
|
let output = Command::new("mkfs.ext4")
|
|
.arg("-d")
|
|
.arg(vfio_path.to_str().unwrap())
|
|
.arg(vfio_disk_path.to_str().unwrap())
|
|
.arg("2g")
|
|
.output()
|
|
.unwrap();
|
|
if !output.status.success() {
|
|
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
|
|
panic!("mkfs.ext4 command generated an error");
|
|
}
|
|
|
|
let mut blk_file_path = workload_path;
|
|
blk_file_path.push("blk.img");
|
|
|
|
let vfio_tap0 = "vfio-tap0";
|
|
let vfio_tap1 = "vfio-tap1";
|
|
let vfio_tap2 = "vfio-tap2";
|
|
let vfio_tap3 = "vfio-tap3";
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=4"])
|
|
.args(["--memory", "size=2G,hugepages=on,shared=on"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args([
|
|
"--disk",
|
|
format!(
|
|
"path={}",
|
|
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
|
|
)
|
|
.as_str(),
|
|
format!(
|
|
"path={}",
|
|
guest.disk_config.disk(DiskType::CloudInit).unwrap()
|
|
)
|
|
.as_str(),
|
|
format!("path={}", vfio_disk_path.to_str().unwrap()).as_str(),
|
|
format!("path={},iommu=on", 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_mac).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();
|
|
|
|
thread::sleep(std::time::Duration::new(30, 0));
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.ssh_command_l1("sudo systemctl start vfio").unwrap();
|
|
thread::sleep(std::time::Duration::new(120, 0));
|
|
|
|
// 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(),
|
|
vec!["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(),
|
|
vec!["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(),
|
|
vec!["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(),
|
|
vec!["{\"id\":\"vfio123\",\"bdf\":\"0000:00:08.0\"}"],
|
|
1
|
|
));
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
|
|
// 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(),
|
|
vec!["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();
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
|
|
// 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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args([
|
|
"--cmdline",
|
|
format!("{DIRECT_KERNEL_BOOT_CMDLINE} acpi=off").as_str(),
|
|
])
|
|
.default_disks()
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1);
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_vsock() {
|
|
_test_virtio_vsock(false)
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_vsock_hotplug() {
|
|
_test_virtio_vsock(true);
|
|
}
|
|
|
|
#[test]
|
|
fn test_api_http_shutdown() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
_test_api_shutdown(TargetApi::new_http_api(&guest.tmp_dir), guest)
|
|
}
|
|
|
|
#[test]
|
|
fn test_api_http_delete() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
_test_api_delete(TargetApi::new_http_api(&guest.tmp_dir), guest);
|
|
}
|
|
|
|
#[test]
|
|
fn test_api_http_pause_resume() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
_test_api_pause_resume(TargetApi::new_http_api(&guest.tmp_dir), guest)
|
|
}
|
|
|
|
#[test]
|
|
fn test_api_http_create_boot() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
_test_api_create_boot(TargetApi::new_http_api(&guest.tmp_dir), 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() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
let kernel_path = direct_kernel_boot_path();
|
|
#[cfg(target_arch = "aarch64")]
|
|
let kernel_path = edk2_path();
|
|
|
|
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])
|
|
.default_disks()
|
|
.args([
|
|
"--net",
|
|
guest.default_net_string().as_str(),
|
|
"tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.0",
|
|
])
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// 2 network interfaces + default localhost ==> 3 interfaces
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("ip -o link | wc -l")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
3
|
|
);
|
|
|
|
let init_bar_addr = guest
|
|
.ssh_command(
|
|
"sudo awk '{print $1; exit}' /sys/bus/pci/devices/0000:00:05.0/resource",
|
|
)
|
|
.unwrap();
|
|
|
|
// Remove the PCI device
|
|
guest
|
|
.ssh_command("echo 1 | sudo tee /sys/bus/pci/devices/0000:00:05.0/remove")
|
|
.unwrap();
|
|
|
|
// Only 1 network interface left + default localhost ==> 2 interfaces
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("ip -o link | wc -l")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
2
|
|
);
|
|
|
|
// Remove the PCI device
|
|
guest
|
|
.ssh_command("echo 1 | sudo tee /sys/bus/pci/rescan")
|
|
.unwrap();
|
|
|
|
// Back to 2 network interface + default localhost ==> 3 interfaces
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("ip -o link | wc -l")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
3
|
|
);
|
|
|
|
let new_bar_addr = guest
|
|
.ssh_command(
|
|
"sudo awk '{print $1; exit}' /sys/bus/pci/devices/0000:00:05.0/resource",
|
|
)
|
|
.unwrap();
|
|
|
|
// Let's compare the BAR addresses for our virtio-net device.
|
|
// They should be different as we expect the BAR reprogramming
|
|
// to have happened.
|
|
assert_ne!(init_bar_addr, new_bar_addr);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_memory_mergeable_off() {
|
|
test_memory_mergeable(false)
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_cpu_hotplug() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
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"])
|
|
.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(None).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();
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert_eq!(
|
|
guest.get_cpu_count().unwrap_or_default(),
|
|
u32::from(desired_vcpus)
|
|
);
|
|
|
|
guest.reboot_linux(0, None);
|
|
|
|
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);
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert_eq!(
|
|
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();
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert_eq!(
|
|
guest.get_cpu_count().unwrap_or_default(),
|
|
u32::from(desired_vcpus)
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_memory_hotplug() {
|
|
#[cfg(target_arch = "aarch64")]
|
|
let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string();
|
|
#[cfg(target_arch = "x86_64")]
|
|
let focal_image = FOCAL_IMAGE_NAME.to_string();
|
|
let focal = UbuntuDiskConfig::new(focal_image);
|
|
let guest = Guest::new(Box::new(focal));
|
|
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(None).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);
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert!(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);
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
|
assert!(guest.get_total_memory().unwrap_or_default() < 960_000);
|
|
|
|
guest.reboot_linux(0, None);
|
|
|
|
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);
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
|
|
assert!(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);
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert!(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, None);
|
|
|
|
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"))]
|
|
fn test_virtio_mem() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
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(None).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);
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert!(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);
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert!(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);
|
|
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 960_000);
|
|
assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000);
|
|
|
|
guest.reboot_linux(0, None);
|
|
|
|
// 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);
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
|
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]
|
|
#[cfg(target_arch = "x86_64")]
|
|
#[cfg(not(feature = "mshv"))]
|
|
// Test both vCPU and memory resizing together
|
|
fn test_resize() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
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(None).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();
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
assert_eq!(
|
|
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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
|
|
let guest_memory_size_kb = 512 * 1024;
|
|
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", format!("size={guest_memory_size_kb}K").as_str()])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_net()
|
|
.default_disks()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
let overhead = get_vmm_overhead(child.id(), guest_memory_size_kb);
|
|
eprintln!("Guest memory overhead: {overhead} vs {MAXIMUM_VMM_OVERHEAD_KB}");
|
|
assert!(overhead <= MAXIMUM_VMM_OVERHEAD_KB);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[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 focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
#[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 child = GuestCommand::new(&guest)
|
|
.args(["--api-socket", &api_socket])
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.args(["--landlock"])
|
|
.default_disks()
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// Check /dev/vdc is not there
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep -c vdc.*16M || true")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or(1),
|
|
0
|
|
);
|
|
|
|
// Now let's add the extra disk.
|
|
let mut blk_file_path = dirs::home_dir().unwrap();
|
|
blk_file_path.push("workloads");
|
|
blk_file_path.push("blk.img");
|
|
// As the path to the hotplug disk is not pre-added, this remote
|
|
// command will fail.
|
|
assert!(!remote_command(
|
|
&api_socket,
|
|
"add-disk",
|
|
Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()),
|
|
));
|
|
});
|
|
|
|
let _ = child.kill();
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
fn _test_disk_hotplug(landlock_enabled: bool) {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
#[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 blk_file_path = dirs::home_dir().unwrap();
|
|
blk_file_path.push("workloads");
|
|
blk_file_path.push("blk.img");
|
|
|
|
let mut cmd = GuestCommand::new(&guest);
|
|
if landlock_enabled {
|
|
cmd.args(["--landlock"]).args([
|
|
"--landlock-rules",
|
|
format!("path={:?},access=rw", blk_file_path).as_str(),
|
|
]);
|
|
}
|
|
|
|
cmd.args(["--api-socket", &api_socket])
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).unwrap();
|
|
|
|
// Check /dev/vdc is not there
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep -c vdc.*16M || true")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or(1),
|
|
0
|
|
);
|
|
|
|
// Now let's add the extra disk.
|
|
let (cmd_success, cmd_output) = remote_command_w_output(
|
|
&api_socket,
|
|
"add-disk",
|
|
Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()),
|
|
);
|
|
assert!(cmd_success);
|
|
assert!(String::from_utf8_lossy(&cmd_output)
|
|
.contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}"));
|
|
|
|
thread::sleep(std::time::Duration::new(10, 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 read.
|
|
guest
|
|
.ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M iflag=direct count=16")
|
|
.unwrap();
|
|
|
|
// Let's remove it the extra disk.
|
|
assert!(remote_command(&api_socket, "remove-device", Some("test0")));
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
// And check /dev/vdc is not there
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep -c vdc.*16M || true")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or(1),
|
|
0
|
|
);
|
|
|
|
// And add it back to validate unplug did work correctly.
|
|
let (cmd_success, cmd_output) = remote_command_w_output(
|
|
&api_socket,
|
|
"add-disk",
|
|
Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()),
|
|
);
|
|
assert!(cmd_success);
|
|
assert!(String::from_utf8_lossy(&cmd_output)
|
|
.contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}"));
|
|
|
|
thread::sleep(std::time::Duration::new(10, 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 read.
|
|
guest
|
|
.ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M iflag=direct count=16")
|
|
.unwrap();
|
|
|
|
// Reboot the VM.
|
|
guest.reboot_linux(0, None);
|
|
|
|
// Check still there after reboot
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep vdc | grep -c 16M")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
1
|
|
);
|
|
|
|
assert!(remote_command(&api_socket, "remove-device", Some("test0")));
|
|
|
|
thread::sleep(std::time::Duration::new(20, 0));
|
|
|
|
// Check device has gone away
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep -c vdc.*16M || true")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or(1),
|
|
0
|
|
);
|
|
|
|
guest.reboot_linux(1, None);
|
|
|
|
// Check device still absent
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep -c vdc.*16M || true")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or(1),
|
|
0
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_disk_hotplug() {
|
|
_test_disk_hotplug(false)
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn test_disk_hotplug_with_landlock() {
|
|
_test_disk_hotplug(true)
|
|
}
|
|
|
|
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();
|
|
|
|
// 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");
|
|
}
|
|
|
|
// Create loop device path
|
|
let 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();
|
|
|
|
// Open backing file
|
|
let backing_file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.open(backing_file_path)
|
|
.unwrap();
|
|
|
|
let loop_config = LoopConfig {
|
|
fd: backing_file.as_raw_fd() as u32,
|
|
block_size,
|
|
..Default::default()
|
|
};
|
|
|
|
for i in 0..num_retries {
|
|
let ret = unsafe {
|
|
libc::ioctl(
|
|
loop_device_file.as_raw_fd(),
|
|
LOOP_CONFIGURE as _,
|
|
&loop_config,
|
|
)
|
|
};
|
|
if ret != 0 {
|
|
if i < num_retries - 1 {
|
|
println!(
|
|
"Iteration {}: Failed to configure the loop device {}: {}",
|
|
i,
|
|
loop_device_path,
|
|
std::io::Error::last_os_error()
|
|
);
|
|
} else {
|
|
panic!(
|
|
"Failed {} times trying to configure the loop device {}: {}",
|
|
num_retries,
|
|
loop_device_path,
|
|
std::io::Error::last_os_error()
|
|
);
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
// Wait for a bit before retrying
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
}
|
|
|
|
loop_device_path
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_block_topology() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
let kernel_path = direct_kernel_boot_path();
|
|
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);
|
|
|
|
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={}", &loop_dev).as_str(),
|
|
])
|
|
.default_net()
|
|
.capture_output()
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
guest.wait_vm_boot(None).unwrap();
|
|
|
|
// MIN-IO column
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk -t| grep vdc | awk '{print $3}'")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
4096
|
|
);
|
|
// PHY-SEC column
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk -t| grep vdc | awk '{print $5}'")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
4096
|
|
);
|
|
// LOG-SEC column
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk -t| grep vdc | awk '{print $6}'")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
4096
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
|
|
Command::new("losetup")
|
|
.args(["-d", &loop_dev])
|
|
.output()
|
|
.expect("loop device not found");
|
|
}
|
|
|
|
#[test]
|
|
fn test_virtio_balloon_deflate_on_oom() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
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])
|
|
.args(["--cpus", "boot=1"])
|
|
.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(None).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.
|
|
thread::sleep(std::time::Duration::new(20, 0));
|
|
|
|
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.
|
|
thread::sleep(std::time::Duration::new(20, 0));
|
|
|
|
// 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"))]
|
|
fn test_virtio_balloon_free_page_reporting() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
//Let's start a 4G guest with balloon occupied 2G memory
|
|
let mut child = GuestCommand::new(&guest)
|
|
.args(["--cpus", "boot=1"])
|
|
.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(None).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_ip.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 50 seconds to make sure the stress command is consuming
|
|
// the expected amount of memory.
|
|
thread::sleep(std::time::Duration::new(50, 0));
|
|
let rss = process_rss_kib(pid);
|
|
println!("RSS {rss} >= 2097152");
|
|
assert!(rss >= 2097152);
|
|
|
|
// Wait for an extra minute to make sure the stress command has
|
|
// completed and that the guest reported the free pages to the VMM
|
|
// through the virtio-balloon device. We expect the RSS to be under
|
|
// 2GiB.
|
|
thread::sleep(std::time::Duration::new(60, 0));
|
|
let rss = process_rss_kib(pid);
|
|
println!("RSS {rss} < 2097152");
|
|
assert!(rss < 2097152);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pmem_hotplug() {
|
|
_test_pmem_hotplug(None)
|
|
}
|
|
|
|
#[test]
|
|
fn test_pmem_multi_segment_hotplug() {
|
|
_test_pmem_hotplug(Some(15))
|
|
}
|
|
|
|
fn _test_pmem_hotplug(pci_segment: Option<u16>) {
|
|
#[cfg(target_arch = "aarch64")]
|
|
let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string();
|
|
#[cfg(target_arch = "x86_64")]
|
|
let focal_image = FOCAL_IMAGE_NAME.to_string();
|
|
let focal = UbuntuDiskConfig::new(focal_image);
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
#[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])
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.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(None).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 {
|
|
"".to_owned()
|
|
}
|
|
)),
|
|
);
|
|
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, None);
|
|
|
|
// 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")));
|
|
|
|
thread::sleep(std::time::Duration::new(20, 0));
|
|
|
|
// Check device has gone away
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("lsblk | grep -c pmem0.*128M || true")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or(1),
|
|
0
|
|
);
|
|
|
|
guest.reboot_linux(1, None);
|
|
|
|
// 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() {
|
|
_test_net_hotplug(None)
|
|
}
|
|
|
|
#[test]
|
|
fn test_net_multi_segment_hotplug() {
|
|
_test_net_hotplug(Some(15))
|
|
}
|
|
|
|
fn _test_net_hotplug(pci_segment: Option<u16>) {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
|
|
#[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])
|
|
.args(["--cpus", "boot=1"])
|
|
.args(["--memory", "size=512M"])
|
|
.args(["--kernel", kernel_path.to_str().unwrap()])
|
|
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
|
|
.default_disks()
|
|
.capture_output();
|
|
|
|
if pci_segment.is_some() {
|
|
cmd.args([
|
|
"--platform",
|
|
&format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"),
|
|
]);
|
|
}
|
|
|
|
let mut child = cmd.spawn().unwrap();
|
|
|
|
thread::sleep(std::time::Duration::new(20, 0));
|
|
|
|
let r = std::panic::catch_unwind(|| {
|
|
// Add network
|
|
let (cmd_success, cmd_output) = remote_command_w_output(
|
|
&api_socket,
|
|
"add-net",
|
|
Some(
|
|
format!(
|
|
"{}{},id=test0",
|
|
guest.default_net_string(),
|
|
if let Some(pci_segment) = pci_segment {
|
|
format!(",pci_segment={pci_segment}")
|
|
} else {
|
|
"".to_owned()
|
|
}
|
|
)
|
|
.as_str(),
|
|
),
|
|
);
|
|
assert!(cmd_success);
|
|
|
|
if let Some(pci_segment) = pci_segment {
|
|
assert!(String::from_utf8_lossy(&cmd_output).contains(&format!(
|
|
"{{\"id\":\"test0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}"
|
|
)));
|
|
} else {
|
|
assert!(String::from_utf8_lossy(&cmd_output)
|
|
.contains("{\"id\":\"test0\",\"bdf\":\"0000:00:05.0\"}"));
|
|
}
|
|
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
|
|
// 1 network interfaces + default localhost ==> 2 interfaces
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("ip -o link | wc -l")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
2
|
|
);
|
|
|
|
// Remove network
|
|
assert!(remote_command(&api_socket, "remove-device", Some("test0"),));
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
|
|
let (cmd_success, cmd_output) = remote_command_w_output(
|
|
&api_socket,
|
|
"add-net",
|
|
Some(
|
|
format!(
|
|
"{}{},id=test1",
|
|
guest.default_net_string(),
|
|
if let Some(pci_segment) = pci_segment {
|
|
format!(",pci_segment={pci_segment}")
|
|
} else {
|
|
"".to_owned()
|
|
}
|
|
)
|
|
.as_str(),
|
|
),
|
|
);
|
|
assert!(cmd_success);
|
|
|
|
if let Some(pci_segment) = pci_segment {
|
|
assert!(String::from_utf8_lossy(&cmd_output).contains(&format!(
|
|
"{{\"id\":\"test1\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}"
|
|
)));
|
|
} else {
|
|
assert!(String::from_utf8_lossy(&cmd_output)
|
|
.contains("{\"id\":\"test1\",\"bdf\":\"0000:00:05.0\"}"));
|
|
}
|
|
|
|
thread::sleep(std::time::Duration::new(5, 0));
|
|
|
|
// 1 network interfaces + default localhost ==> 2 interfaces
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("ip -o link | wc -l")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
2
|
|
);
|
|
|
|
guest.reboot_linux(0, None);
|
|
|
|
// Check still there after reboot
|
|
// 1 network interfaces + default localhost ==> 2 interfaces
|
|
assert_eq!(
|
|
guest
|
|
.ssh_command("ip -o link | wc -l")
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default(),
|
|
2
|
|
);
|
|
});
|
|
|
|
kill_child(&mut child);
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
handle_child_output(r, &output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_initramfs() {
|
|
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
|
|
let guest = Guest::new(Box::new(focal));
|
|
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");
|
|
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([" |