mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
Focal has served us well for many years but is now beyond EOL. Remove all remaining use of focal images from the CI. Signed-off-by: Rob Bradford <rbradford@meta.com>
2725 lines
89 KiB
Rust
2725 lines
89 KiB
Rust
// Copyright © 2021 Intel Corporation
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
#![allow(clippy::undocumented_unsafe_blocks)]
|
|
|
|
use std::collections::HashMap;
|
|
use std::ffi::OsStr;
|
|
use std::fmt::{Display, Formatter};
|
|
use std::fs::OpenOptions;
|
|
use std::io::{Read, Seek, SeekFrom, Write};
|
|
use std::net::{TcpListener, TcpStream};
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::os::unix::io::{AsRawFd, FromRawFd};
|
|
use std::os::unix::process::CommandExt;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Child, Command, ExitStatus, Output, Stdio};
|
|
use std::str::FromStr;
|
|
use std::sync::{LazyLock, Mutex};
|
|
use std::time::{Duration, Instant};
|
|
use std::{env, fmt, fs, io, thread};
|
|
|
|
use rand::Rng;
|
|
use serde_json::Value;
|
|
use ssh2::Session;
|
|
use thiserror::Error;
|
|
use vmm_sys_util::tempdir::TempDir;
|
|
use wait_timeout::ChildExt;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Process group registry, one group per test.
|
|
//
|
|
// Every child process spawned during a test is placed into a shared
|
|
// process group. The first child creates the group via setpgid(0, 0)
|
|
// and subsequent children join via setpgid(0, pgid). A single
|
|
// killpg(pgid, SIGKILL) tears down all processes when the test ends.
|
|
//
|
|
// The registry maps test name to the group PID, protected by a mutex
|
|
// so that concurrent tests each get their own independent group.
|
|
//
|
|
// Any Command::spawn() in test helpers should go through
|
|
// ProcessRegistry::spawn so the child is automatically tracked.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static PROCESS_REGISTRY: LazyLock<Mutex<HashMap<String, u32>>> =
|
|
LazyLock::new(|| Mutex::new(HashMap::new()));
|
|
|
|
pub struct ProcessRegistry;
|
|
|
|
impl ProcessRegistry {
|
|
/// Spawn `cmd` in the process group for `test_name`.
|
|
///
|
|
/// The first spawn creates a new group (group PID = child PID).
|
|
/// Subsequent spawns join the existing group.
|
|
pub fn spawn(test_name: &str, cmd: &mut Command) -> io::Result<Child> {
|
|
let pgid = {
|
|
let mut reg = PROCESS_REGISTRY.lock().unwrap();
|
|
let stored = reg.get(test_name).copied();
|
|
// If the stored group no longer has any live processes,
|
|
// discard it so the next child creates a fresh group.
|
|
if let Some(id) = stored {
|
|
let probe = unsafe { libc::killpg(id as i32, 0) };
|
|
if probe == -1 && io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
|
|
reg.remove(test_name);
|
|
None
|
|
} else {
|
|
stored
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
unsafe {
|
|
cmd.pre_exec(move || {
|
|
let target = pgid.unwrap_or(0) as libc::pid_t;
|
|
// Best effort: may fail with EPERM in containers.
|
|
let _ = libc::setpgid(0, target);
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
let child = cmd.spawn()?;
|
|
|
|
if pgid.is_none() {
|
|
let actual = unsafe { libc::getpgid(child.id() as i32) };
|
|
if actual == child.id() as i32 {
|
|
PROCESS_REGISTRY
|
|
.lock()
|
|
.unwrap()
|
|
.insert(test_name.to_string(), child.id());
|
|
}
|
|
}
|
|
|
|
Ok(child)
|
|
}
|
|
|
|
/// Kill all processes in the group for `test_name` and remove
|
|
/// the entry.
|
|
pub fn cleanup(test_name: &str) -> bool {
|
|
let pgid = PROCESS_REGISTRY.lock().unwrap().remove(test_name);
|
|
|
|
if let Some(pgid) = pgid {
|
|
let ret = unsafe { libc::killpg(pgid as i32, libc::SIGKILL) };
|
|
if ret == 0 {
|
|
eprintln!(
|
|
"[cleanup] Sent SIGKILL to process group {pgid} \
|
|
(test '{test_name}')"
|
|
);
|
|
return true;
|
|
}
|
|
let err = io::Error::last_os_error();
|
|
// ESRCH: all processes already exited.
|
|
if err.raw_os_error() != Some(libc::ESRCH) {
|
|
eprintln!(
|
|
"[cleanup] Failed to kill process group {pgid} \
|
|
(test '{test_name}'): {err}"
|
|
);
|
|
}
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum WaitTimeoutError {
|
|
#[error("timeout")]
|
|
Timedout,
|
|
#[error("exit status indicates failure")]
|
|
ExitStatus,
|
|
#[error("general failure")]
|
|
General(#[source] std::io::Error),
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum Error {
|
|
#[error("Failed to parse")]
|
|
Parsing(#[source] std::num::ParseIntError),
|
|
#[error("ssh command failed")]
|
|
SshCommand(#[from] SshCommandError),
|
|
#[error("waiting for boot failed")]
|
|
WaitForBoot(#[source] WaitForBootError),
|
|
#[error("reading log file failed")]
|
|
EthrLogFile(#[source] std::io::Error),
|
|
#[error("parsing log file failed")]
|
|
EthrLogParse,
|
|
#[error("parsing fio output failed")]
|
|
FioOutputParse,
|
|
#[error("parsing iperf3 output failed")]
|
|
Iperf3Parse,
|
|
#[error("spawning process failed")]
|
|
Spawn(#[source] std::io::Error),
|
|
#[error("waiting for timeout failed")]
|
|
WaitTimeout(#[source] WaitTimeoutError),
|
|
}
|
|
|
|
/// Polls a boolean condition until it becomes true or the timeout expires.
|
|
pub fn wait_until<F>(timeout: Duration, mut condition: F) -> bool
|
|
where
|
|
F: FnMut() -> bool,
|
|
{
|
|
const INTERVAL: Duration = Duration::from_millis(50);
|
|
let start = Instant::now();
|
|
|
|
loop {
|
|
if condition() {
|
|
return true;
|
|
}
|
|
|
|
if start.elapsed() >= timeout {
|
|
return false;
|
|
}
|
|
|
|
thread::sleep(INTERVAL);
|
|
}
|
|
}
|
|
|
|
/// Retries an operation until it returns `Ok` or the timeout expires.
|
|
pub fn wait_until_succeeds<F, T, E>(timeout: Duration, mut operation: F) -> Result<T, E>
|
|
where
|
|
F: FnMut() -> Result<T, E>,
|
|
{
|
|
const INTERVAL: Duration = Duration::from_millis(50);
|
|
let start = Instant::now();
|
|
|
|
loop {
|
|
match operation() {
|
|
Ok(result) => return Ok(result),
|
|
Err(err) if start.elapsed() >= timeout => return Err(err),
|
|
Err(_) => thread::sleep(INTERVAL),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct GuestNetworkConfig {
|
|
pub guest_ip0: String,
|
|
pub host_ip0: String,
|
|
pub guest_mac0: String,
|
|
pub guest_ip1: String,
|
|
pub host_ip1: String,
|
|
pub guest_mac1: String,
|
|
pub l2_guest_ip1: String,
|
|
pub l2_guest_ip2: String,
|
|
pub l2_guest_ip3: String,
|
|
pub l2_guest_mac1: String,
|
|
pub l2_guest_mac2: String,
|
|
pub l2_guest_mac3: String,
|
|
pub tcp_listener_port: u16,
|
|
pub l2_tcp_listener_port: u16,
|
|
pub notify_ip: String,
|
|
}
|
|
|
|
pub const DEFAULT_TCP_LISTENER_MESSAGE: &str = "booted";
|
|
pub const DEFAULT_TCP_LISTENER_PORT: u16 = 8000;
|
|
pub const DEFAULT_TCP_LISTENER_TIMEOUT: u32 = 120;
|
|
pub const DEFAULT_CVM_TCP_LISTENER_TIMEOUT: u32 = 140;
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum WaitForBootError {
|
|
#[error("Failed to wait for epoll")]
|
|
EpollWait(#[source] std::io::Error),
|
|
#[error("Failed to listen for boot")]
|
|
Listen(#[source] std::io::Error),
|
|
#[error("Epoll wait timeout")]
|
|
EpollWaitTimeout,
|
|
#[error("wrong guest address")]
|
|
WrongGuestAddr,
|
|
#[error("Failed to accept a TCP request")]
|
|
Accept(#[source] std::io::Error),
|
|
}
|
|
|
|
impl GuestNetworkConfig {
|
|
pub fn wait_vm_boot_from(
|
|
port: u16,
|
|
expected_guest_addr: &str,
|
|
custom_timeout: u32,
|
|
) -> Result<(), WaitForBootError> {
|
|
let start = std::time::Instant::now();
|
|
let listen_addr = format!("0.0.0.0:{port}");
|
|
let mut s = String::new();
|
|
|
|
let mut closure = || -> Result<(), WaitForBootError> {
|
|
let listener =
|
|
TcpListener::bind(listen_addr.as_str()).map_err(WaitForBootError::Listen)?;
|
|
listener
|
|
.set_nonblocking(true)
|
|
.expect("Cannot set non-blocking for tcp listener");
|
|
|
|
// Reply on epoll w/ timeout to wait for guest connections faithfully
|
|
let epoll_fd = epoll::create(true).expect("Cannot create epoll fd");
|
|
// Use 'File' to enforce closing on 'epoll_fd'
|
|
let _epoll_file = unsafe { fs::File::from_raw_fd(epoll_fd) };
|
|
epoll::ctl(
|
|
epoll_fd,
|
|
epoll::ControlOptions::EPOLL_CTL_ADD,
|
|
listener.as_raw_fd(),
|
|
epoll::Event::new(epoll::Events::EPOLLIN, 0),
|
|
)
|
|
.expect("Cannot add 'tcp_listener' event to epoll");
|
|
let mut events = [epoll::Event::new(epoll::Events::empty(), 0); 1];
|
|
loop {
|
|
let num_events = match epoll::wait(
|
|
epoll_fd,
|
|
(custom_timeout * 1000).try_into().unwrap(),
|
|
&mut events[..],
|
|
) {
|
|
Ok(num_events) => Ok(num_events),
|
|
Err(e) => match e.raw_os_error() {
|
|
Some(libc::EAGAIN) | Some(libc::EINTR) => continue,
|
|
_ => Err(e),
|
|
},
|
|
}
|
|
.map_err(WaitForBootError::EpollWait)?;
|
|
if num_events == 0 {
|
|
return Err(WaitForBootError::EpollWaitTimeout);
|
|
}
|
|
break;
|
|
}
|
|
|
|
match listener.accept() {
|
|
Ok((_, addr)) => {
|
|
// Make sure the connection is from the expected 'guest_addr'
|
|
if addr.ip() != std::net::IpAddr::from_str(expected_guest_addr).unwrap() {
|
|
s = format!(
|
|
"Expecting the guest ip '{}' while being connected with ip '{}'",
|
|
expected_guest_addr,
|
|
addr.ip()
|
|
);
|
|
return Err(WaitForBootError::WrongGuestAddr);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
s = "TcpListener::accept() failed".to_string();
|
|
Err(WaitForBootError::Accept(e))
|
|
}
|
|
}
|
|
};
|
|
|
|
match closure() {
|
|
Err(e) => {
|
|
let duration = start.elapsed();
|
|
eprintln!(
|
|
"\n\n==== Start 'wait_vm_boot' (FAILED) ==== \
|
|
\n\nduration =\"{duration:?}, timeout = {custom_timeout}s\" \
|
|
\nlisten_addr=\"{listen_addr}\" \
|
|
\nexpected_guest_addr=\"{expected_guest_addr}\" \
|
|
\nmessage=\"{s}\" \
|
|
\nerror=\"{e:?}\" \
|
|
\n\n==== End 'wait_vm_boot' outout ====\n\n"
|
|
);
|
|
|
|
Err(e)
|
|
}
|
|
Ok(_) => Ok(()),
|
|
}
|
|
}
|
|
|
|
pub fn wait_vm_boot(&self, custom_timeout: u32) -> Result<(), WaitForBootError> {
|
|
Self::wait_vm_boot_from(self.tcp_listener_port, &self.guest_ip0, custom_timeout)
|
|
}
|
|
}
|
|
|
|
pub enum DiskType {
|
|
OperatingSystem,
|
|
CloudInit,
|
|
}
|
|
|
|
pub trait DiskConfig {
|
|
fn prepare_files(&mut self, tmp_dir: &TempDir, network: &GuestNetworkConfig);
|
|
fn prepare_cloudinit(&self, tmp_dir: &TempDir, network: &GuestNetworkConfig) -> String;
|
|
fn disk(&self, disk_type: DiskType) -> Option<String>;
|
|
fn qcow2_disk(&self) -> Option<String> {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct UbuntuDiskConfig {
|
|
osdisk_path: String,
|
|
cloudinit_path: String,
|
|
image_name: String,
|
|
}
|
|
|
|
impl UbuntuDiskConfig {
|
|
pub fn new(image_name: String) -> Self {
|
|
UbuntuDiskConfig {
|
|
image_name,
|
|
osdisk_path: String::new(),
|
|
cloudinit_path: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct WindowsDiskConfig {
|
|
image_name: String,
|
|
osdisk_path: String,
|
|
osdisk_qcow2_path: String,
|
|
loopback_device: String,
|
|
windows_snapshot_cow: String,
|
|
windows_snapshot: String,
|
|
}
|
|
|
|
impl WindowsDiskConfig {
|
|
pub fn new(image_name: String) -> Self {
|
|
WindowsDiskConfig {
|
|
image_name,
|
|
osdisk_path: String::new(),
|
|
osdisk_qcow2_path: String::new(),
|
|
loopback_device: String::new(),
|
|
windows_snapshot_cow: String::new(),
|
|
windows_snapshot: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for WindowsDiskConfig {
|
|
fn drop(&mut self) {
|
|
// dmsetup remove windows-snapshot-1
|
|
std::process::Command::new("dmsetup")
|
|
.arg("remove")
|
|
.arg(self.windows_snapshot.as_str())
|
|
.output()
|
|
.expect("Expect removing Windows snapshot with 'dmsetup' to succeed");
|
|
|
|
// dmsetup remove windows-snapshot-cow-1
|
|
std::process::Command::new("dmsetup")
|
|
.arg("remove")
|
|
.arg(self.windows_snapshot_cow.as_str())
|
|
.output()
|
|
.expect("Expect removing Windows snapshot CoW with 'dmsetup' to succeed");
|
|
|
|
// losetup -d <loopback_device>
|
|
std::process::Command::new("losetup")
|
|
.args(["-d", self.loopback_device.as_str()])
|
|
.output()
|
|
.expect("Expect removing loopback device to succeed");
|
|
|
|
if !self.osdisk_qcow2_path.is_empty() {
|
|
let _ = fs::remove_file(&self.osdisk_qcow2_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the workspace root directory.
|
|
///
|
|
/// As we don't have packages in the workspace root,
|
|
/// we walk up until we found the main Cargo.toml file.
|
|
fn workspace_root() -> PathBuf {
|
|
// The directory of the current crate (integration test).
|
|
let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
|
|
// Currently we have one level of nesting and we probably never change it.
|
|
let max_levels = 2;
|
|
|
|
eprintln!(
|
|
"Looking for workspace root: starting with dir={}",
|
|
dir.to_str().unwrap()
|
|
);
|
|
|
|
// walk up
|
|
for _ in 0..max_levels {
|
|
dir = dir.parent().unwrap().to_path_buf();
|
|
eprintln!("Checking parent dir: {}", dir.to_str().unwrap());
|
|
let maybe_manifest_file = dir.join("Cargo.toml");
|
|
if maybe_manifest_file.exists() {
|
|
let content = fs::read_to_string(&maybe_manifest_file).unwrap();
|
|
if content.contains("[workspace]") && content.contains("Cloud Hypervisor Workspace") {
|
|
eprintln!("INFO: Found workspace root: {}", dir.to_str().unwrap());
|
|
return dir;
|
|
}
|
|
}
|
|
}
|
|
|
|
panic!("Could not find workspace root");
|
|
}
|
|
|
|
impl DiskConfig for UbuntuDiskConfig {
|
|
fn prepare_cloudinit(&self, tmp_dir: &TempDir, network: &GuestNetworkConfig) -> String {
|
|
let cloudinit_file_path =
|
|
String::from(tmp_dir.as_path().join("cloudinit").to_str().unwrap());
|
|
|
|
let cloud_init_directory = tmp_dir.as_path().join("cloud-init").join("ubuntu");
|
|
|
|
fs::create_dir_all(&cloud_init_directory)
|
|
.expect("Expect creating cloud-init directory to succeed");
|
|
|
|
let source_file_dir = workspace_root()
|
|
.join("test_data")
|
|
.join("cloud-init")
|
|
.join("ubuntu")
|
|
.join("ci");
|
|
|
|
["meta-data"].iter().for_each(|x| {
|
|
let source_file = source_file_dir.join(x);
|
|
let cloud_init = cloud_init_directory.join(x);
|
|
rate_limited_copy(source_file, cloud_init)
|
|
.expect("Expect copying cloud-init meta-data to succeed");
|
|
});
|
|
|
|
let mut user_data_string = String::new();
|
|
fs::File::open(source_file_dir.join("user-data"))
|
|
.unwrap()
|
|
.read_to_string(&mut user_data_string)
|
|
.expect("Expected reading user-data file to succeed");
|
|
user_data_string = user_data_string.replace(
|
|
"@DEFAULT_TCP_LISTENER_MESSAGE",
|
|
DEFAULT_TCP_LISTENER_MESSAGE,
|
|
);
|
|
user_data_string = user_data_string.replace("@NOTIFY_IP", &network.notify_ip);
|
|
user_data_string =
|
|
user_data_string.replace("@TCP_LISTENER_PORT", &network.tcp_listener_port.to_string());
|
|
|
|
fs::File::create(cloud_init_directory.join("user-data"))
|
|
.unwrap()
|
|
.write_all(user_data_string.as_bytes())
|
|
.expect("Expected writing out user-data to succeed");
|
|
|
|
let mut network_config_string = String::new();
|
|
|
|
fs::File::open(source_file_dir.join("network-config"))
|
|
.unwrap()
|
|
.read_to_string(&mut network_config_string)
|
|
.expect("Expected reading network-config file to succeed");
|
|
|
|
network_config_string = network_config_string.replace("192.168.2.1", &network.host_ip0);
|
|
network_config_string = network_config_string.replace("192.168.2.2", &network.guest_ip0);
|
|
network_config_string = network_config_string.replace("192.168.2.129", &network.host_ip1);
|
|
network_config_string = network_config_string.replace("192.168.2.130", &network.guest_ip1);
|
|
network_config_string = network_config_string.replace("192.168.2.3", &network.l2_guest_ip1);
|
|
network_config_string = network_config_string.replace("192.168.2.4", &network.l2_guest_ip2);
|
|
network_config_string = network_config_string.replace("192.168.2.5", &network.l2_guest_ip3);
|
|
network_config_string =
|
|
network_config_string.replace("12:34:56:78:90:ab", &network.guest_mac0);
|
|
network_config_string =
|
|
network_config_string.replace("de:ad:be:ef:78:90", &network.guest_mac1);
|
|
network_config_string =
|
|
network_config_string.replace("de:ad:be:ef:12:34", &network.l2_guest_mac1);
|
|
network_config_string =
|
|
network_config_string.replace("de:ad:be:ef:34:56", &network.l2_guest_mac2);
|
|
network_config_string =
|
|
network_config_string.replace("de:ad:be:ef:56:78", &network.l2_guest_mac3);
|
|
|
|
fs::File::create(cloud_init_directory.join("network-config"))
|
|
.unwrap()
|
|
.write_all(network_config_string.as_bytes())
|
|
.expect("Expected writing out network-config to succeed");
|
|
|
|
std::process::Command::new("mkdosfs")
|
|
.args(["-n", "CIDATA"])
|
|
.args(["-C", cloudinit_file_path.as_str()])
|
|
.arg("8192")
|
|
.output()
|
|
.expect("Expect creating disk image to succeed");
|
|
|
|
["user-data", "meta-data", "network-config"]
|
|
.iter()
|
|
.for_each(|x| {
|
|
std::process::Command::new("mcopy")
|
|
.arg("-o")
|
|
.args(["-i", cloudinit_file_path.as_str()])
|
|
.args(["-s", cloud_init_directory.join(x).to_str().unwrap(), "::"])
|
|
.output()
|
|
.expect("Expect copying files to disk image to succeed");
|
|
});
|
|
|
|
cloudinit_file_path
|
|
}
|
|
|
|
fn prepare_files(&mut self, tmp_dir: &TempDir, network: &GuestNetworkConfig) {
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut osdisk_base_path = workload_path;
|
|
osdisk_base_path.push(&self.image_name);
|
|
|
|
let osdisk_path = String::from(tmp_dir.as_path().join("osdisk.img").to_str().unwrap());
|
|
let cloudinit_path = self.prepare_cloudinit(tmp_dir, network);
|
|
|
|
rate_limited_copy(osdisk_base_path, &osdisk_path)
|
|
.expect("copying of OS source disk image failed");
|
|
|
|
self.cloudinit_path = cloudinit_path;
|
|
self.osdisk_path = osdisk_path;
|
|
}
|
|
|
|
fn disk(&self, disk_type: DiskType) -> Option<String> {
|
|
match disk_type {
|
|
DiskType::OperatingSystem => Some(self.osdisk_path.clone()),
|
|
DiskType::CloudInit => Some(self.cloudinit_path.clone()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DiskConfig for WindowsDiskConfig {
|
|
fn prepare_cloudinit(&self, _tmp_dir: &TempDir, _network: &GuestNetworkConfig) -> String {
|
|
String::new()
|
|
}
|
|
|
|
fn prepare_files(&mut self, tmp_dir: &TempDir, _network: &GuestNetworkConfig) {
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
|
|
let mut osdisk_path = workload_path;
|
|
osdisk_path.push(&self.image_name);
|
|
|
|
let osdisk_blk_size = fs::metadata(&osdisk_path)
|
|
.expect("Expect retrieving Windows image metadata")
|
|
.len()
|
|
>> 9;
|
|
|
|
let snapshot_cow_path =
|
|
String::from(tmp_dir.as_path().join("snapshot_cow").to_str().unwrap());
|
|
|
|
// Create and truncate CoW file for device mapper
|
|
let cow_file_size: u64 = 1 << 30;
|
|
let cow_file_blk_size = cow_file_size >> 9;
|
|
let cow_file = std::fs::File::create(snapshot_cow_path.as_str())
|
|
.expect("Expect creating CoW image to succeed");
|
|
cow_file
|
|
.set_len(cow_file_size)
|
|
.expect("Expect truncating CoW image to succeed");
|
|
|
|
// losetup --find --show /tmp/snapshot_cow
|
|
let loopback_device = std::process::Command::new("losetup")
|
|
.arg("--find")
|
|
.arg("--show")
|
|
.arg(snapshot_cow_path.as_str())
|
|
.output()
|
|
.expect("Expect creating loopback device from snapshot CoW image to succeed");
|
|
|
|
self.loopback_device = String::from_utf8_lossy(&loopback_device.stdout)
|
|
.trim()
|
|
.to_string();
|
|
|
|
let random_extension = tmp_dir.as_path().file_name().unwrap();
|
|
let windows_snapshot_cow = format!(
|
|
"windows-snapshot-cow-{}",
|
|
random_extension.to_str().unwrap()
|
|
);
|
|
|
|
// dmsetup create windows-snapshot-cow-1 --table '0 2097152 linear /dev/loop1 0'
|
|
std::process::Command::new("dmsetup")
|
|
.arg("create")
|
|
.arg(windows_snapshot_cow.as_str())
|
|
.args([
|
|
"--table",
|
|
format!("0 {} linear {} 0", cow_file_blk_size, self.loopback_device).as_str(),
|
|
])
|
|
.output()
|
|
.expect("Expect creating Windows snapshot CoW with 'dmsetup' to succeed");
|
|
|
|
let windows_snapshot = format!("windows-snapshot-{}", random_extension.to_str().unwrap());
|
|
|
|
// dmsetup mknodes
|
|
std::process::Command::new("dmsetup")
|
|
.arg("mknodes")
|
|
.output()
|
|
.expect("Expect device mapper nodes to be ready");
|
|
|
|
// dmsetup create windows-snapshot-1 --table '0 41943040 snapshot /dev/mapper/windows-base /dev/mapper/windows-snapshot-cow-1 P 8'
|
|
std::process::Command::new("dmsetup")
|
|
.arg("create")
|
|
.arg(windows_snapshot.as_str())
|
|
.args([
|
|
"--table",
|
|
format!(
|
|
"0 {} snapshot /dev/mapper/windows-base /dev/mapper/{} P 8",
|
|
osdisk_blk_size,
|
|
windows_snapshot_cow.as_str()
|
|
)
|
|
.as_str(),
|
|
])
|
|
.output()
|
|
.expect("Expect creating Windows snapshot with 'dmsetup' to succeed");
|
|
|
|
// dmsetup mknodes
|
|
std::process::Command::new("dmsetup")
|
|
.arg("mknodes")
|
|
.output()
|
|
.expect("Expect device mapper nodes to be ready");
|
|
|
|
self.osdisk_path = format!("/dev/mapper/{windows_snapshot}");
|
|
self.windows_snapshot_cow = windows_snapshot_cow;
|
|
self.windows_snapshot = windows_snapshot;
|
|
|
|
// Create a qcow2 overlay backed by the raw image.
|
|
let mut workload_path = dirs::home_dir().unwrap();
|
|
workload_path.push("workloads");
|
|
let qcow2_name = format!("windows-qcow2-{}.qcow2", random_extension.to_str().unwrap());
|
|
let qcow2_path = workload_path.join(&qcow2_name);
|
|
let output = Command::new("qemu-img")
|
|
.args([
|
|
"create",
|
|
"-f",
|
|
"qcow2",
|
|
"-b",
|
|
osdisk_path.to_str().unwrap(),
|
|
"-F",
|
|
"raw",
|
|
qcow2_path.to_str().unwrap(),
|
|
])
|
|
.output()
|
|
.expect("Expect creating qcow2 overlay to succeed");
|
|
assert!(output.status.success(), "qemu-img create failed");
|
|
self.osdisk_qcow2_path = qcow2_path.to_str().unwrap().to_string();
|
|
}
|
|
|
|
fn disk(&self, disk_type: DiskType) -> Option<String> {
|
|
match disk_type {
|
|
DiskType::OperatingSystem => Some(self.osdisk_path.clone()),
|
|
DiskType::CloudInit => None,
|
|
}
|
|
}
|
|
|
|
fn qcow2_disk(&self) -> Option<String> {
|
|
Some(self.osdisk_qcow2_path.clone())
|
|
}
|
|
}
|
|
|
|
pub fn rate_limited_copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<u64> {
|
|
for i in 0..10 {
|
|
let free_bytes = unsafe {
|
|
let mut stats = std::mem::MaybeUninit::zeroed();
|
|
let fs_name = std::ffi::CString::new("/tmp").unwrap();
|
|
libc::statvfs(fs_name.as_ptr(), stats.as_mut_ptr());
|
|
|
|
let free_blocks = stats.assume_init().f_bfree;
|
|
let block_size = stats.assume_init().f_bsize;
|
|
|
|
free_blocks * block_size
|
|
};
|
|
|
|
// Make sure there is at least 6 GiB of space
|
|
if free_bytes < 6 << 30 {
|
|
eprintln!("Not enough space on disk ({free_bytes}). Attempt {i} of 10. Sleeping.");
|
|
thread::sleep(std::time::Duration::new(60, 0));
|
|
continue;
|
|
}
|
|
|
|
match fs::copy(&from, &to) {
|
|
Err(e) => {
|
|
if let Some(errno) = e.raw_os_error()
|
|
&& errno == libc::ENOSPC
|
|
{
|
|
eprintln!("Copy returned ENOSPC. Attempt {i} of 10. Sleeping.");
|
|
thread::sleep(std::time::Duration::new(60, 0));
|
|
continue;
|
|
}
|
|
|
|
return Err(e);
|
|
}
|
|
Ok(i) => return Ok(i),
|
|
}
|
|
}
|
|
Err(io::Error::last_os_error())
|
|
}
|
|
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
pub fn handle_child_output(
|
|
r: Result<(), std::boxed::Box<dyn std::any::Any + std::marker::Send>>,
|
|
output: &std::process::Output,
|
|
) {
|
|
use std::os::unix::process::ExitStatusExt;
|
|
if r.is_ok() && output.status.success() {
|
|
return;
|
|
}
|
|
|
|
match output.status.code() {
|
|
None => {
|
|
// Don't treat child.kill() as a problem
|
|
if output.status.signal() == Some(9) && r.is_ok() {
|
|
return;
|
|
}
|
|
|
|
eprintln!(
|
|
"==== child killed by signal: {} ====",
|
|
output.status.signal().unwrap()
|
|
);
|
|
}
|
|
Some(code) => {
|
|
eprintln!("\n\n==== child exit code: {code} ====");
|
|
}
|
|
}
|
|
|
|
eprintln!(
|
|
"\n\n==== Start child stdout ====\n\n{}\n\n==== End child stdout ====",
|
|
String::from_utf8_lossy(&output.stdout)
|
|
);
|
|
eprintln!(
|
|
"\n\n==== Start child stderr ====\n\n{}\n\n==== End child stderr ====",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
|
|
panic!("Test failed")
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct PasswordAuth {
|
|
pub username: String,
|
|
pub password: String,
|
|
}
|
|
|
|
pub const DEFAULT_SSH_RETRIES: u8 = 6;
|
|
pub const DEFAULT_SSH_TIMEOUT: u8 = 10;
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum SshCommandError {
|
|
#[error("ssh connection failed")]
|
|
Connection(#[source] std::io::Error),
|
|
#[error("ssh handshake failed")]
|
|
Handshake(#[source] ssh2::Error),
|
|
#[error("ssh authentication failed")]
|
|
Authentication(#[source] ssh2::Error),
|
|
#[error("ssh channel session failed")]
|
|
ChannelSession(#[source] ssh2::Error),
|
|
#[error("ssh command failed")]
|
|
Command(#[source] ssh2::Error),
|
|
#[error("retrieving exit status from ssh command failed")]
|
|
ExitStatus(#[source] ssh2::Error),
|
|
#[error("the exit code indicates failure: {0}")]
|
|
NonZeroExitStatus(i32),
|
|
#[error("failed to read file")]
|
|
FileRead(#[source] std::io::Error),
|
|
#[error("failed to read metadata")]
|
|
FileMetadata(#[source] std::io::Error),
|
|
#[error("scp send failed")]
|
|
ScpSend(#[source] ssh2::Error),
|
|
#[error("scp write failed")]
|
|
WriteAll(#[source] std::io::Error),
|
|
#[error("scp send EOF failed")]
|
|
SendEof(#[source] ssh2::Error),
|
|
#[error("scp wait EOF failed")]
|
|
WaitEof(#[source] ssh2::Error),
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum WaitForSshError {
|
|
#[error("timed out after {timeout:?} waiting for ssh command {command:?} on {ip}: {source}")]
|
|
Timeout {
|
|
command: String,
|
|
ip: String,
|
|
timeout: Duration,
|
|
#[source]
|
|
source: SshCommandError,
|
|
},
|
|
}
|
|
|
|
pub fn default_guest_auth() -> PasswordAuth {
|
|
PasswordAuth {
|
|
username: String::from("cloud"),
|
|
password: String::from("cloud123"),
|
|
}
|
|
}
|
|
|
|
fn scp_to_guest_with_auth(
|
|
path: &Path,
|
|
remote_path: &Path,
|
|
auth: &PasswordAuth,
|
|
ip: &str,
|
|
retries: u8,
|
|
timeout: u8,
|
|
) -> Result<(), SshCommandError> {
|
|
let mut counter = 0;
|
|
loop {
|
|
let closure = || -> Result<(), SshCommandError> {
|
|
let tcp =
|
|
TcpStream::connect(format!("{ip}:22")).map_err(SshCommandError::Connection)?;
|
|
let mut sess = Session::new().unwrap();
|
|
sess.set_tcp_stream(tcp);
|
|
sess.handshake().map_err(SshCommandError::Handshake)?;
|
|
|
|
sess.userauth_password(&auth.username, &auth.password)
|
|
.map_err(SshCommandError::Authentication)?;
|
|
assert!(sess.authenticated());
|
|
|
|
let content = fs::read(path).map_err(SshCommandError::FileRead)?;
|
|
let mode = fs::metadata(path)
|
|
.map_err(SshCommandError::FileMetadata)?
|
|
.permissions()
|
|
.mode()
|
|
& 0o777;
|
|
|
|
let mut channel = sess
|
|
.scp_send(remote_path, mode as i32, content.len() as u64, None)
|
|
.map_err(SshCommandError::ScpSend)?;
|
|
channel
|
|
.write_all(&content)
|
|
.map_err(SshCommandError::WriteAll)?;
|
|
channel.send_eof().map_err(SshCommandError::SendEof)?;
|
|
channel.wait_eof().map_err(SshCommandError::WaitEof)?;
|
|
|
|
// Intentionally ignore these results here as their failure
|
|
// does not precipitate a repeat
|
|
let _ = channel.close();
|
|
let _ = channel.wait_close();
|
|
|
|
Ok(())
|
|
};
|
|
|
|
match closure() {
|
|
Ok(_) => break,
|
|
Err(e) => {
|
|
counter += 1;
|
|
if counter >= retries {
|
|
eprintln!(
|
|
"\n\n==== Start scp command output (FAILED) ====\n\n\
|
|
path =\"{path:?}\"\n\
|
|
remote_path =\"{remote_path:?}\"\n\
|
|
auth=\"{auth:#?}\"\n\
|
|
ip=\"{ip}\"\n\
|
|
error=\"{e:?}\"\n\
|
|
\n==== End scp command outout ====\n\n"
|
|
);
|
|
|
|
return Err(e);
|
|
}
|
|
}
|
|
}
|
|
thread::sleep(std::time::Duration::new((timeout * counter).into(), 0));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn scp_to_guest(
|
|
path: &Path,
|
|
remote_path: &Path,
|
|
ip: &str,
|
|
retries: u8,
|
|
timeout: u8,
|
|
) -> Result<(), SshCommandError> {
|
|
scp_to_guest_with_auth(
|
|
path,
|
|
remote_path,
|
|
&PasswordAuth {
|
|
username: String::from("cloud"),
|
|
password: String::from("cloud123"),
|
|
},
|
|
ip,
|
|
retries,
|
|
timeout,
|
|
)
|
|
}
|
|
|
|
/// Executes a command on a remote host via SSH using password authentication.
|
|
/// Returns the stdout output on success, or an [`SshCommandError`] on any
|
|
/// connection, authentication, or execution failure.
|
|
pub fn ssh_command_ip_with_auth(
|
|
command: &str,
|
|
auth: &PasswordAuth,
|
|
ip: &str,
|
|
timeout: Option<Duration>,
|
|
) -> Result<String, SshCommandError> {
|
|
let mut s = String::new();
|
|
let tcp = TcpStream::connect(format!("{ip}:22")).map_err(SshCommandError::Connection)?;
|
|
let mut sess = Session::new().unwrap();
|
|
sess.set_tcp_stream(tcp);
|
|
if let Some(timeout) = timeout {
|
|
sess.set_timeout(timeout.as_millis() as u32);
|
|
}
|
|
sess.handshake().map_err(SshCommandError::Handshake)?;
|
|
sess.userauth_password(&auth.username, &auth.password)
|
|
.map_err(SshCommandError::Authentication)?;
|
|
assert!(sess.authenticated());
|
|
let mut channel = sess
|
|
.channel_session()
|
|
.map_err(SshCommandError::ChannelSession)?;
|
|
channel.exec(command).map_err(SshCommandError::Command)?;
|
|
// Intentionally ignore these results here as their failure
|
|
// does not precipitate a repeat
|
|
let _ = channel.read_to_string(&mut s);
|
|
let _ = channel.close();
|
|
let _ = channel.wait_close();
|
|
let status = channel.exit_status().map_err(SshCommandError::ExitStatus)?;
|
|
if status != 0 {
|
|
Err(SshCommandError::NonZeroExitStatus(status))
|
|
} else {
|
|
Ok(s)
|
|
}
|
|
}
|
|
|
|
/// Executes a command on a remote host via SSH using password authentication,
|
|
/// retrying on failure with linear backoff.
|
|
///
|
|
/// Delegates each attempt to [`ssh_command_ip_with_auth`]. After the
|
|
/// *n*-th consecutive failure the function sleeps for `timeout_s * n` seconds
|
|
/// before the next attempt. Once `retries` attempts are exhausted the command
|
|
/// output and error are printed to stderr and the last error is returned.
|
|
///
|
|
/// Note that `timeout_s` is not a per-attempt deadline — individual connection
|
|
/// and I/O operations may block for as long as the OS or SSH layer allows.
|
|
// TODO since we have we probably want to migrate every single invocation to a
|
|
// more graceful combination of wait_until() and ssh_command_ip_with_auth().
|
|
pub fn ssh_command_ip_with_auth_retry(
|
|
command: &str,
|
|
auth: &PasswordAuth,
|
|
ip: &str,
|
|
retries: u8,
|
|
// Base unit for the inter-retry sleep duration, in seconds.
|
|
timeout_s: u8,
|
|
) -> Result<String, SshCommandError> {
|
|
let mut counter = 0;
|
|
loop {
|
|
match ssh_command_ip_with_auth(command, auth, ip, None) {
|
|
Ok(s) => return Ok(s),
|
|
Err(e) => {
|
|
counter += 1;
|
|
if counter >= retries {
|
|
eprintln!(
|
|
"\n\n==== Start ssh command output (FAILED) ====\n\n\
|
|
command=\"{command}\"\n\
|
|
auth=\"{auth:#?}\"\n\
|
|
ip=\"{ip}\"\n\
|
|
error=\"{e:?}\"\n\
|
|
\n==== End ssh command output ====\n\n"
|
|
);
|
|
return Err(e);
|
|
}
|
|
}
|
|
}
|
|
thread::sleep(std::time::Duration::new((timeout_s * counter).into(), 0));
|
|
}
|
|
}
|
|
|
|
/// Executes a command on a remote host via SSH using password authentication,
|
|
/// retrying on failure with linear backoff.
|
|
///
|
|
/// Wrapper around [`ssh_command_ip_with_auth_retry`].
|
|
pub fn ssh_command_ip(
|
|
command: &str,
|
|
ip: &str,
|
|
retries: u8,
|
|
timeout: u8,
|
|
) -> Result<String, SshCommandError> {
|
|
ssh_command_ip_with_auth_retry(
|
|
command,
|
|
&PasswordAuth {
|
|
username: String::from("cloud"),
|
|
password: String::from("cloud123"),
|
|
},
|
|
ip,
|
|
retries,
|
|
timeout,
|
|
)
|
|
}
|
|
|
|
/// Waits until SSH to the guest becomes available.
|
|
pub fn wait_for_ssh(
|
|
command: &str,
|
|
auth: &PasswordAuth,
|
|
ip: &str,
|
|
timeout: Duration,
|
|
) -> Result<String, WaitForSshError> {
|
|
wait_until_succeeds(timeout, || {
|
|
ssh_command_ip_with_auth(command, auth, ip, Some(timeout))
|
|
})
|
|
.map_err(|source| WaitForSshError::Timeout {
|
|
command: command.to_string(),
|
|
ip: ip.to_string(),
|
|
timeout,
|
|
source,
|
|
})
|
|
}
|
|
|
|
pub fn exec_host_command_with_retries(command: &str, retries: u32, interval: Duration) -> bool {
|
|
for _ in 0..retries {
|
|
let s = exec_host_command_output(command).status;
|
|
if s.success() {
|
|
return true;
|
|
}
|
|
eprintln!("\n\n==== retrying in {interval:?} ===\n\n");
|
|
thread::sleep(interval);
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub fn exec_host_command_status(command: &str) -> ExitStatus {
|
|
exec_host_command_output(command).status
|
|
}
|
|
|
|
pub fn exec_host_command_output(command: &str) -> Output {
|
|
let output = std::process::Command::new("bash")
|
|
.args(["-c", command])
|
|
.output()
|
|
.unwrap_or_else(|e| panic!("Expected '{command}' to run. Error: {e:?}"));
|
|
|
|
if !output.status.success() {
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
eprintln!(
|
|
"\n\n==== Start 'exec_host_command' failed ==== \
|
|
\n\n---stdout---\n{stdout}\n---stderr---{stderr} \
|
|
\n\n==== End 'exec_host_command' failed ====",
|
|
);
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
pub fn check_lines_count(input: &str, line_count: usize) -> bool {
|
|
if input.lines().count() == line_count {
|
|
true
|
|
} else {
|
|
eprintln!(
|
|
"\n\n==== Start 'check_lines_count' failed ==== \
|
|
\n\ninput = {input}\nline_count = {line_count} \
|
|
\n\n==== End 'check_lines_count' failed ====",
|
|
);
|
|
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn check_matched_lines_count(input: &str, keywords: &[&str], line_count: usize) -> bool {
|
|
let mut matches = String::new();
|
|
for line in input.lines() {
|
|
if keywords.iter().all(|k| line.contains(k)) {
|
|
matches += line;
|
|
}
|
|
}
|
|
|
|
if matches.lines().count() == line_count {
|
|
true
|
|
} else {
|
|
eprintln!(
|
|
"\n\n==== Start 'check_matched_lines_count' failed ==== \
|
|
\nkeywords = {keywords:?}, line_count = {line_count} \
|
|
\n\ninput = {input} matches = {matches} \
|
|
\n\n==== End 'check_matched_lines_count' failed ====",
|
|
);
|
|
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn kill_child(child: &mut Child) {
|
|
let r = unsafe { libc::kill(child.id() as i32, libc::SIGTERM) };
|
|
if r != 0 {
|
|
let e = io::Error::last_os_error();
|
|
if e.raw_os_error().unwrap() == libc::ESRCH {
|
|
return;
|
|
}
|
|
eprintln!("Failed to kill child with SIGTERM: {e:?}");
|
|
}
|
|
|
|
// The timeout period elapsed without the child exiting
|
|
if child.wait_timeout(Duration::new(10, 0)).unwrap().is_none() {
|
|
let _ = child.kill();
|
|
let rust_flags = env::var("RUSTFLAGS").unwrap_or_default();
|
|
if rust_flags.contains("-Cinstrument-coverage") {
|
|
panic!("Wait child timeout, please check the reason.")
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct MetaEvent {
|
|
pub event: String,
|
|
pub 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
|
|
}
|
|
}
|
|
|
|
pub const PIPE_SIZE: i32 = 32 << 20;
|
|
|
|
pub struct Guest {
|
|
pub tmp_dir: TempDir,
|
|
pub disk_config: Box<dyn DiskConfig>,
|
|
pub network: GuestNetworkConfig,
|
|
pub vm_type: GuestVmType,
|
|
pub boot_timeout: u32,
|
|
pub kernel_path: Option<String>,
|
|
pub kernel_cmdline: Option<String>,
|
|
pub console_type: Option<String>,
|
|
pub num_cpu: u32,
|
|
pub nested: bool,
|
|
pub mem_size_str: String,
|
|
/// Test name, set from the current thread name at construction.
|
|
pub test_name: Option<String>,
|
|
}
|
|
|
|
// Return the next id that can be used for this guest. This is stored in a
|
|
// file in the filesystem and is protected by a filesystem lock allowing
|
|
// multiple test processes to safely access it with the process blocking
|
|
// until the lock is released.
|
|
fn next_guest_id() -> u8 {
|
|
let mut id_file_path = dirs::home_dir().unwrap();
|
|
id_file_path.push("workloads");
|
|
id_file_path.push("id.counter");
|
|
|
|
let mut id_file = OpenOptions::new()
|
|
.create(true)
|
|
.truncate(false)
|
|
.write(true)
|
|
.read(true)
|
|
.open(id_file_path)
|
|
.unwrap();
|
|
|
|
id_file.lock().unwrap();
|
|
|
|
// Use a string in the file for (human) readability
|
|
let mut buf = String::default();
|
|
id_file.read_to_string(&mut buf).unwrap();
|
|
let id = buf.trim().parse::<u8>().unwrap_or(1);
|
|
let next_id = u8::max(1, id.overflowing_add(1).0);
|
|
id_file.set_len(0).unwrap();
|
|
id_file.seek(SeekFrom::Start(0)).unwrap();
|
|
id_file.write_all(next_id.to_string().as_bytes()).unwrap();
|
|
|
|
id_file.unlock().unwrap();
|
|
|
|
id
|
|
}
|
|
|
|
// Safe to implement as we know we have no interior mutability
|
|
impl std::panic::RefUnwindSafe for Guest {}
|
|
|
|
impl Guest {
|
|
pub fn new_from_ip_range(mut disk_config: Box<dyn DiskConfig>, class: &str, id: u8) -> Self {
|
|
let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap();
|
|
|
|
let network = GuestNetworkConfig {
|
|
guest_ip0: format!("{class}.{id}.2"),
|
|
host_ip0: format!("{class}.{id}.1"),
|
|
guest_mac0: format!("12:34:56:78:90:{id:02x}"),
|
|
guest_ip1: format!("{class}.{id}.130"),
|
|
host_ip1: format!("{class}.{id}.129"),
|
|
guest_mac1: format!("de:ad:be:ef:78:{id:02x}"),
|
|
l2_guest_ip1: format!("{class}.{id}.3"),
|
|
l2_guest_ip2: format!("{class}.{id}.4"),
|
|
l2_guest_ip3: format!("{class}.{id}.5"),
|
|
l2_guest_mac1: format!("de:ad:be:ef:12:{id:02x}"),
|
|
l2_guest_mac2: format!("de:ad:be:ef:34:{id:02x}"),
|
|
l2_guest_mac3: format!("de:ad:be:ef:56:{id:02x}"),
|
|
tcp_listener_port: DEFAULT_TCP_LISTENER_PORT + id as u16,
|
|
l2_tcp_listener_port: DEFAULT_TCP_LISTENER_PORT + 1024 + id as u16,
|
|
notify_ip: format!("{class}.{id}.1"),
|
|
};
|
|
|
|
disk_config.prepare_files(&tmp_dir, &network);
|
|
|
|
Guest {
|
|
tmp_dir,
|
|
disk_config,
|
|
network,
|
|
vm_type: GuestVmType::Regular,
|
|
boot_timeout: DEFAULT_TCP_LISTENER_TIMEOUT,
|
|
kernel_path: direct_kernel_boot_path().to_str().map(String::from),
|
|
kernel_cmdline: Some(DIRECT_KERNEL_BOOT_CMDLINE.to_string()),
|
|
console_type: None,
|
|
num_cpu: 1u32,
|
|
nested: true,
|
|
mem_size_str: "512M".to_string(),
|
|
test_name: thread::current().name().map(String::from),
|
|
}
|
|
}
|
|
|
|
pub fn new(disk_config: Box<dyn DiskConfig>) -> Self {
|
|
Self::new_from_ip_range(disk_config, "192.168", next_guest_id())
|
|
}
|
|
|
|
pub fn with_cpu(mut self, count: u32) -> Self {
|
|
self.num_cpu = count;
|
|
self
|
|
}
|
|
|
|
pub fn with_memory(mut self, mem_size: &str) -> Self {
|
|
self.mem_size_str = mem_size.to_string();
|
|
self
|
|
}
|
|
|
|
pub fn with_nested(mut self, nested: bool) -> Self {
|
|
self.nested = nested;
|
|
self
|
|
}
|
|
|
|
pub fn with_kernel_path(mut self, kernel_path: &str) -> Self {
|
|
self.kernel_path = Some(kernel_path.to_string());
|
|
self
|
|
}
|
|
|
|
pub fn with_kernel(mut self, kernel: String) -> Self {
|
|
self.kernel_path = Some(kernel);
|
|
self
|
|
}
|
|
|
|
pub fn default_net_string(&self) -> String {
|
|
format!(
|
|
"tap=,mac={},ip={},mask=255.255.255.128",
|
|
self.network.guest_mac0, self.network.host_ip0
|
|
)
|
|
}
|
|
|
|
pub fn default_net_string_w_iommu(&self) -> String {
|
|
format!(
|
|
"tap=,mac={},ip={},mask=255.255.255.128,iommu=on",
|
|
self.network.guest_mac0, self.network.host_ip0
|
|
)
|
|
}
|
|
|
|
pub fn default_net_string_w_mtu(&self, mtu: u16) -> String {
|
|
format!(
|
|
"tap=,mac={},ip={},mask=255.255.255.128,mtu={}",
|
|
self.network.guest_mac0, self.network.host_ip0, mtu
|
|
)
|
|
}
|
|
|
|
pub fn ssh_command(&self, command: &str) -> Result<String, SshCommandError> {
|
|
ssh_command_ip(
|
|
command,
|
|
&self.network.guest_ip0,
|
|
DEFAULT_SSH_RETRIES,
|
|
DEFAULT_SSH_TIMEOUT,
|
|
)
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
pub fn ssh_command_l1(&self, command: &str) -> Result<String, SshCommandError> {
|
|
ssh_command_ip(
|
|
command,
|
|
&self.network.guest_ip0,
|
|
DEFAULT_SSH_RETRIES,
|
|
DEFAULT_SSH_TIMEOUT,
|
|
)
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
pub fn ssh_command_l2_1(&self, command: &str) -> Result<String, SshCommandError> {
|
|
ssh_command_ip(
|
|
command,
|
|
&self.network.l2_guest_ip1,
|
|
DEFAULT_SSH_RETRIES,
|
|
DEFAULT_SSH_TIMEOUT,
|
|
)
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
pub fn ssh_command_l2_2(&self, command: &str) -> Result<String, SshCommandError> {
|
|
ssh_command_ip(
|
|
command,
|
|
&self.network.l2_guest_ip2,
|
|
DEFAULT_SSH_RETRIES,
|
|
DEFAULT_SSH_TIMEOUT,
|
|
)
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
pub fn ssh_command_l2_3(&self, command: &str) -> Result<String, SshCommandError> {
|
|
ssh_command_ip(
|
|
command,
|
|
&self.network.l2_guest_ip3,
|
|
DEFAULT_SSH_RETRIES,
|
|
DEFAULT_SSH_TIMEOUT,
|
|
)
|
|
}
|
|
|
|
/// Waits until SSH to the guest becomes available using the
|
|
/// [default guest authentication] and the default guest IP.
|
|
///
|
|
/// [default guest authentication]: default_guest_auth
|
|
pub fn wait_for_ssh(&self, timeout: Duration) -> Result<(), WaitForSshError> {
|
|
wait_for_ssh(
|
|
"true",
|
|
&default_guest_auth(),
|
|
&self.network.guest_ip0,
|
|
timeout,
|
|
)
|
|
.map(|_| ())
|
|
}
|
|
|
|
/// Waits until the provided command succeeds via SSH on the guest using the
|
|
/// [default guest authentication] and the default guest IP.
|
|
///
|
|
/// [default guest authentication]: default_guest_auth
|
|
pub fn wait_for_ssh_command(
|
|
&self,
|
|
command: &str,
|
|
timeout: Duration,
|
|
) -> Result<String, WaitForSshError> {
|
|
wait_for_ssh(
|
|
command,
|
|
&default_guest_auth(),
|
|
&self.network.guest_ip0,
|
|
timeout,
|
|
)
|
|
}
|
|
|
|
/// Waits until the guest's SSH port is no longer reachable, indicating
|
|
/// the guest has probably shutdown.
|
|
pub fn wait_for_ssh_unresponsive(&self, timeout: Duration) -> bool {
|
|
let addr = format!("{}:22", self.network.guest_ip0)
|
|
.parse::<std::net::SocketAddr>()
|
|
.unwrap();
|
|
wait_until(timeout, || {
|
|
std::net::TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_err()
|
|
})
|
|
}
|
|
|
|
pub fn api_create_body(&self) -> String {
|
|
let mut body = serde_json::json!({
|
|
"cpus": {
|
|
"boot_vcpus": self.num_cpu,
|
|
"max_vcpus": self.num_cpu,
|
|
},
|
|
"net": [
|
|
{
|
|
"ip": self.network.host_ip0,
|
|
"mask": "255.255.255.0",
|
|
"mac": self.network.guest_mac0,
|
|
}
|
|
],
|
|
"disks": [
|
|
{
|
|
"path": self.disk_config.disk(DiskType::OperatingSystem).unwrap(),
|
|
},
|
|
{
|
|
"path": self.disk_config.disk(DiskType::CloudInit).unwrap(),
|
|
}
|
|
]
|
|
});
|
|
|
|
if !self.nested {
|
|
body["cpus"]["nested"] = serde_json::json!(false);
|
|
}
|
|
|
|
if self.vm_type == GuestVmType::Confidential {
|
|
body["platform"] = serde_json::json!({"sev_snp": true});
|
|
body["payload"] = serde_json::json!({
|
|
"igvm": direct_igvm_boot_path(Some("hvc0"))
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap(),
|
|
"cmdline": self.kernel_cmdline.as_deref().unwrap(),
|
|
"host_data": generate_host_data(),
|
|
});
|
|
} else {
|
|
body["payload"] = serde_json::json!({
|
|
"kernel": self.kernel_path.as_deref().unwrap(),
|
|
"cmdline": self.kernel_cmdline.as_deref().unwrap(),
|
|
});
|
|
}
|
|
|
|
body.to_string()
|
|
}
|
|
|
|
pub fn get_cpu_count(&self) -> Result<u32, Error> {
|
|
self.ssh_command("grep -c processor /proc/cpuinfo")?
|
|
.trim()
|
|
.parse()
|
|
.map_err(Error::Parsing)
|
|
}
|
|
|
|
pub fn get_total_memory(&self) -> Result<u32, Error> {
|
|
self.ssh_command("grep MemTotal /proc/meminfo | grep -o \"[0-9]*\"")?
|
|
.trim()
|
|
.parse()
|
|
.map_err(Error::Parsing)
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
pub fn get_total_memory_l2(&self) -> Result<u32, Error> {
|
|
self.ssh_command_l2_1("grep MemTotal /proc/meminfo | grep -o \"[0-9]*\"")?
|
|
.trim()
|
|
.parse()
|
|
.map_err(Error::Parsing)
|
|
}
|
|
|
|
pub fn get_numa_node_memory(&self, node_id: usize) -> Result<u32, Error> {
|
|
self.ssh_command(
|
|
format!(
|
|
"grep MemTotal /sys/devices/system/node/node{node_id}/meminfo \
|
|
| cut -d \":\" -f 2 | grep -o \"[0-9]*\""
|
|
)
|
|
.as_str(),
|
|
)?
|
|
.trim()
|
|
.parse()
|
|
.map_err(Error::Parsing)
|
|
}
|
|
|
|
fn default_boot_timeout(&self) -> u32 {
|
|
self.boot_timeout
|
|
}
|
|
|
|
pub fn wait_vm_boot(&self) -> Result<(), Error> {
|
|
self.network
|
|
.wait_vm_boot(self.default_boot_timeout())
|
|
.map_err(Error::WaitForBoot)
|
|
}
|
|
|
|
pub fn wait_vm_boot_custom_timeout(&self, custom_timeout: u32) -> Result<(), Error> {
|
|
self.network
|
|
.wait_vm_boot(custom_timeout)
|
|
.map_err(Error::WaitForBoot)
|
|
}
|
|
|
|
pub fn prepare_l2_cloudinit(&self) -> (TempDir, String) {
|
|
let l2_dir = TempDir::new_with_prefix("/tmp/ch-l2").unwrap();
|
|
let l2_network = GuestNetworkConfig {
|
|
tcp_listener_port: self.network.l2_tcp_listener_port,
|
|
..self.network.clone()
|
|
};
|
|
let path = self.disk_config.prepare_cloudinit(&l2_dir, &l2_network);
|
|
(l2_dir, path)
|
|
}
|
|
|
|
pub fn check_numa_node_cpus(&self, node_id: usize, cpus: &[usize]) -> Result<(), Error> {
|
|
for cpu in cpus.iter() {
|
|
let cmd = format!("[ -d \"/sys/devices/system/node/node{node_id}/cpu{cpu}\" ]");
|
|
self.ssh_command(cmd.as_str())?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn check_numa_node_distances(
|
|
&self,
|
|
node_id: usize,
|
|
distances: &str,
|
|
) -> Result<bool, Error> {
|
|
let cmd = format!("cat /sys/devices/system/node/node{node_id}/distance");
|
|
if self.ssh_command(cmd.as_str())?.trim() == distances {
|
|
Ok(true)
|
|
} else {
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
pub fn check_numa_common(
|
|
&self,
|
|
mem_ref: Option<&[u32]>,
|
|
node_ref: Option<&[&[usize]]>,
|
|
distance_ref: Option<&[&str]>,
|
|
) {
|
|
if let Some(mem_ref) = mem_ref {
|
|
// Check each NUMA node has been assigned the right amount of
|
|
// memory.
|
|
for (i, &m) in mem_ref.iter().enumerate() {
|
|
assert!(self.get_numa_node_memory(i).unwrap_or_default() > m);
|
|
}
|
|
}
|
|
|
|
if let Some(node_ref) = node_ref {
|
|
// Check each NUMA node has been assigned the right CPUs set.
|
|
for (i, n) in node_ref.iter().enumerate() {
|
|
self.check_numa_node_cpus(i, n).unwrap();
|
|
}
|
|
}
|
|
|
|
if let Some(distance_ref) = distance_ref {
|
|
// Check each NUMA node has been assigned the right distances.
|
|
for (i, &d) in distance_ref.iter().enumerate() {
|
|
assert!(self.check_numa_node_distances(i, d).unwrap());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_pci_bridge_class(&self) -> Result<String, Error> {
|
|
Ok(self
|
|
.ssh_command("cat /sys/bus/pci/devices/0000:00:00.0/class")?
|
|
.trim()
|
|
.to_string())
|
|
}
|
|
|
|
pub fn get_pci_device_ids(&self) -> Result<String, Error> {
|
|
Ok(self
|
|
.ssh_command("cat /sys/bus/pci/devices/*/device")?
|
|
.trim()
|
|
.to_string())
|
|
}
|
|
|
|
pub fn get_pci_vendor_ids(&self) -> Result<String, Error> {
|
|
Ok(self
|
|
.ssh_command("cat /sys/bus/pci/devices/*/vendor")?
|
|
.trim()
|
|
.to_string())
|
|
}
|
|
|
|
pub fn does_device_vendor_pair_match(
|
|
&self,
|
|
device_id: &str,
|
|
vendor_id: &str,
|
|
) -> Result<bool, Error> {
|
|
// We are checking if console device's device id and vendor id pair matches
|
|
let devices = self.get_pci_device_ids()?;
|
|
let devices: Vec<&str> = devices.split('\n').collect();
|
|
let vendors = self.get_pci_vendor_ids()?;
|
|
let vendors: Vec<&str> = vendors.split('\n').collect();
|
|
|
|
for (index, d_id) in devices.iter().enumerate() {
|
|
if *d_id == device_id
|
|
&& let Some(v_id) = vendors.get(index)
|
|
&& *v_id == vendor_id
|
|
{
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
pub fn check_vsock(&self, socket: &str) {
|
|
// Listen from guest on vsock CID=3 PORT=16
|
|
// SOCKET-LISTEN:<domain>:<protocol>:<local-address>
|
|
let guest_ip = self.network.guest_ip0.clone();
|
|
let listen_socat = thread::spawn(move || {
|
|
ssh_command_ip("sudo socat - SOCKET-LISTEN:40:0:x00x00x10x00x00x00x03x00x00x00x00x00x00x00 > vsock_log", &guest_ip, DEFAULT_SSH_RETRIES, DEFAULT_SSH_TIMEOUT).unwrap();
|
|
});
|
|
|
|
// Make sure socat is listening, which might take a few second on slow systems
|
|
thread::sleep(std::time::Duration::new(10, 0));
|
|
|
|
// Write something to vsock from the host
|
|
assert!(
|
|
exec_host_command_status(&format!(
|
|
"echo -e \"CONNECT 16\\nHelloWorld!\" | socat - UNIX-CONNECT:{socket}"
|
|
))
|
|
.success()
|
|
);
|
|
|
|
// Wait for the thread to terminate.
|
|
listen_socat.join().unwrap();
|
|
|
|
assert_eq!(
|
|
self.ssh_command("cat vsock_log").unwrap().trim(),
|
|
"HelloWorld!"
|
|
);
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
pub fn check_nvidia_gpu(&self) -> bool {
|
|
let output = self.ssh_command("nvidia-smi").unwrap();
|
|
|
|
if output.contains("NVIDIA L40S") {
|
|
return true;
|
|
}
|
|
|
|
let dmesg = self
|
|
.ssh_command("sudo dmesg")
|
|
.unwrap_or_else(|e| format!("Failed to get dmesg: {e:?}"));
|
|
|
|
eprintln!(
|
|
"\n\n==== Guest dmesg (nvidia-smi check failed) ====\n\n\
|
|
{dmesg}\n\
|
|
\n==== End guest dmesg ====\n\n"
|
|
);
|
|
eprintln!("nvidia-smi output did not contain 'NVIDIA L40S': {output}");
|
|
|
|
false
|
|
}
|
|
|
|
pub fn reboot_linux(&self, current_reboot_count: u32) {
|
|
let list_boots_cmd = "sudo last | grep -c reboot";
|
|
let boot_count = self
|
|
.ssh_command(list_boots_cmd)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default();
|
|
|
|
assert_eq!(boot_count, current_reboot_count + 1);
|
|
self.ssh_command("sudo reboot").unwrap();
|
|
|
|
self.wait_vm_boot().unwrap();
|
|
let boot_count = self
|
|
.ssh_command(list_boots_cmd)
|
|
.unwrap()
|
|
.trim()
|
|
.parse::<u32>()
|
|
.unwrap_or_default();
|
|
assert_eq!(boot_count, current_reboot_count + 2);
|
|
}
|
|
|
|
pub fn enable_memory_hotplug(&self) {
|
|
self.ssh_command("echo online | sudo tee /sys/devices/system/memory/auto_online_blocks")
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn check_devices_common(
|
|
&self,
|
|
socket: Option<&String>,
|
|
console_text: Option<&String>,
|
|
pmem_path: Option<&String>,
|
|
) {
|
|
// Check block devices are readable
|
|
self.ssh_command("sudo dd if=/dev/vda of=/dev/null bs=1M iflag=direct count=1024")
|
|
.unwrap();
|
|
self.ssh_command("sudo dd if=/dev/vdb of=/dev/null bs=1M iflag=direct count=8")
|
|
.unwrap();
|
|
// Check if the rng device is readable
|
|
self.ssh_command("sudo head -c 1000 /dev/hwrng > /dev/null")
|
|
.unwrap();
|
|
// Check vsock
|
|
if let Some(socket) = socket {
|
|
self.check_vsock(socket.as_str());
|
|
}
|
|
// Check if the console is usable
|
|
if let Some(console_text) = console_text {
|
|
let console_cmd = format!("echo {console_text} | sudo tee /dev/hvc0");
|
|
self.ssh_command(&console_cmd).unwrap();
|
|
}
|
|
// The net device is 'automatically' exercised through the above 'ssh' commands
|
|
|
|
// Check if the pmem device is usable
|
|
if let Some(pmem_path) = pmem_path {
|
|
assert_eq!(
|
|
self.ssh_command(&format!("ls {pmem_path}")).unwrap().trim(),
|
|
pmem_path
|
|
);
|
|
assert_eq!(
|
|
self.ssh_command(&format!("sudo mount {pmem_path} /mnt"))
|
|
.unwrap(),
|
|
""
|
|
);
|
|
assert_eq!(self.ssh_command("ls /mnt").unwrap(), "lost+found\n");
|
|
self.ssh_command("echo test123 | sudo tee /mnt/test")
|
|
.unwrap();
|
|
assert_eq!(self.ssh_command("sudo umount /mnt").unwrap(), "");
|
|
assert_eq!(self.ssh_command("ls /mnt").unwrap(), "");
|
|
|
|
assert_eq!(
|
|
self.ssh_command(&format!("sudo mount {pmem_path} /mnt"))
|
|
.unwrap(),
|
|
""
|
|
);
|
|
assert_eq!(
|
|
self.ssh_command("sudo cat /mnt/test || true")
|
|
.unwrap()
|
|
.trim(),
|
|
"test123"
|
|
);
|
|
self.ssh_command("sudo rm /mnt/test").unwrap();
|
|
assert_eq!(self.ssh_command("sudo umount /mnt").unwrap(), "");
|
|
}
|
|
}
|
|
|
|
pub fn get_expected_seq_events_for_simple_launch(&self) -> Vec<MetaEvent> {
|
|
let mut out_evt = vec![
|
|
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()),
|
|
},
|
|
];
|
|
// For confidential VM, reset of the device does not trigger a VMM exit, or
|
|
// It is handled in the PSP
|
|
// so we won't receive the "reset" event for disk0.
|
|
if self.vm_type != GuestVmType::Confidential {
|
|
out_evt.push(MetaEvent {
|
|
event: "reset".to_string(),
|
|
device_id: Some("_disk0".to_string()),
|
|
});
|
|
}
|
|
out_evt
|
|
}
|
|
|
|
pub fn default_cpus_string(&self) -> String {
|
|
format!(
|
|
"boot={}{}",
|
|
self.num_cpu,
|
|
if self.nested { "" } else { ",nested=off" }
|
|
)
|
|
}
|
|
|
|
pub fn default_cpus_with_affinity_string(&self) -> String {
|
|
format!(
|
|
"boot={},affinity=[0@[0,2],1@[1,3]]{}",
|
|
self.num_cpu,
|
|
if self.nested { "" } else { ",nested=off" }
|
|
)
|
|
}
|
|
|
|
pub fn default_memory_string(&self) -> String {
|
|
format!("size={}", self.mem_size_str)
|
|
}
|
|
|
|
pub fn validate_cpu_count(&self, expected_cpu_count: Option<u32>) {
|
|
let cpu = match expected_cpu_count {
|
|
Some(count) => count,
|
|
None => self.num_cpu,
|
|
};
|
|
assert_eq!(self.get_cpu_count().unwrap_or_default(), cpu);
|
|
}
|
|
|
|
fn get_expected_memory(&self) -> Option<u32> {
|
|
// For confidential VMs, the memory available to the guest is less than
|
|
// the memory assigned to the VM, as some of it is reserved for the PSP
|
|
// and bounce buffers.
|
|
// So we return the expected available memory for confidential VMs here.
|
|
let memory = match self.mem_size_str.as_str() {
|
|
"512M" => {
|
|
if self.vm_type == GuestVmType::Confidential {
|
|
407_000
|
|
} else {
|
|
400_000
|
|
}
|
|
}
|
|
"1G" => {
|
|
if self.vm_type == GuestVmType::Confidential {
|
|
920_000
|
|
} else {
|
|
960_000
|
|
}
|
|
}
|
|
// More to be added if more memory sizes are used in the tests
|
|
_ => panic!("Unsupported memory size: {}", self.mem_size_str),
|
|
};
|
|
Some(memory)
|
|
}
|
|
|
|
pub fn validate_memory(&self, expected_memory: Option<u32>) {
|
|
let memory = expected_memory
|
|
.or_else(|| self.get_expected_memory())
|
|
.unwrap_or_default();
|
|
|
|
assert!(self.get_total_memory().unwrap_or_default() > memory);
|
|
}
|
|
}
|
|
|
|
// A factory for creating guests with different configurations. The factory is initialized
|
|
// with a GuestVmType, and created guests will have the same GuestVmType as the factory.
|
|
// This allows creation of guests with different configurations (e.g. regular vs confidential)
|
|
// without specifying the GuestVmType each time.
|
|
// Based on the VmType, the default timeout for waiting for the VM to boot is also set,
|
|
// which is used in the wait_vm_boot() method of the Guest struct. Additionally, nested
|
|
// virtualization is disabled by default for confidential VMs, as it is not supported.
|
|
pub struct GuestFactory {
|
|
vm_type: GuestVmType,
|
|
boot_timeout: u32,
|
|
nested: bool,
|
|
}
|
|
|
|
impl GuestFactory {
|
|
pub fn new_regular_guest_factory() -> Self {
|
|
Self {
|
|
vm_type: GuestVmType::Regular,
|
|
boot_timeout: DEFAULT_TCP_LISTENER_TIMEOUT,
|
|
nested: true,
|
|
}
|
|
}
|
|
|
|
pub fn new_confidential_guest_factory() -> Self {
|
|
Self {
|
|
vm_type: GuestVmType::Confidential,
|
|
boot_timeout: DEFAULT_CVM_TCP_LISTENER_TIMEOUT,
|
|
nested: false,
|
|
}
|
|
}
|
|
|
|
pub fn create_guest(&self, disk_config: Box<dyn DiskConfig>) -> Guest {
|
|
let mut guest = Guest::new(disk_config);
|
|
guest.vm_type = self.vm_type;
|
|
guest.boot_timeout = self.boot_timeout;
|
|
guest.nested = self.nested;
|
|
guest
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub enum VerbosityLevel {
|
|
#[default]
|
|
Warn,
|
|
Info,
|
|
Debug,
|
|
}
|
|
|
|
impl Display for VerbosityLevel {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
use VerbosityLevel::*;
|
|
match self {
|
|
Warn => (),
|
|
Info => write!(f, "-v")?,
|
|
Debug => write!(f, "-vv")?,
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub struct GuestCommand<'a> {
|
|
command: Command,
|
|
guest: &'a Guest,
|
|
capture_output: bool,
|
|
print_cmd: bool,
|
|
verbosity: VerbosityLevel,
|
|
}
|
|
|
|
impl<'a> GuestCommand<'a> {
|
|
pub fn new(guest: &'a Guest) -> Self {
|
|
Self::new_with_binary_path(guest, &clh_command("cloud-hypervisor"))
|
|
}
|
|
|
|
pub fn new_with_binary_path(guest: &'a Guest, binary_path: &str) -> Self {
|
|
Self {
|
|
command: Command::new(binary_path),
|
|
guest,
|
|
capture_output: false,
|
|
print_cmd: true,
|
|
verbosity: VerbosityLevel::Info,
|
|
}
|
|
}
|
|
|
|
pub fn verbosity(&mut self, verbosity: VerbosityLevel) -> &mut Self {
|
|
self.verbosity = verbosity;
|
|
self
|
|
}
|
|
|
|
pub fn capture_output(&mut self) -> &mut Self {
|
|
self.capture_output = true;
|
|
self
|
|
}
|
|
|
|
pub fn set_print_cmd(&mut self, print_cmd: bool) -> &mut Self {
|
|
self.print_cmd = print_cmd;
|
|
self
|
|
}
|
|
|
|
pub fn spawn(&mut self) -> io::Result<Child> {
|
|
use VerbosityLevel::*;
|
|
match &self.verbosity {
|
|
Warn => {}
|
|
Info => {
|
|
self.command.arg("-v");
|
|
}
|
|
Debug => {
|
|
self.command.args(["-vv"]);
|
|
}
|
|
}
|
|
|
|
if self.print_cmd {
|
|
println!(
|
|
"\n\n==== Start cloud-hypervisor command-line ====\n\n\
|
|
{:?}\n\
|
|
\n==== End cloud-hypervisor command-line ====\n\n",
|
|
self.command
|
|
);
|
|
}
|
|
|
|
if self.capture_output {
|
|
self.command.stderr(Stdio::piped()).stdout(Stdio::piped());
|
|
|
|
// The caller should call .wait() on the returned child
|
|
#[allow(unknown_lints)]
|
|
#[allow(clippy::zombie_processes)]
|
|
let child = if let Some(name) = &self.guest.test_name {
|
|
ProcessRegistry::spawn(name, &mut self.command)?
|
|
} else {
|
|
self.command.spawn().unwrap()
|
|
};
|
|
|
|
let fd = child.stdout.as_ref().unwrap().as_raw_fd();
|
|
let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) };
|
|
if pipesize == -1 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
let fd = child.stderr.as_ref().unwrap().as_raw_fd();
|
|
let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) };
|
|
if pipesize1 == -1 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
|
|
if pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE {
|
|
Ok(child)
|
|
} else {
|
|
Err(std::io::Error::other(format!(
|
|
"resizing pipe w/ 'fnctl' failed: stdout pipesize {pipesize}, stderr pipesize {pipesize1}"
|
|
)))
|
|
}
|
|
} else {
|
|
// The caller should call .wait() on the returned child
|
|
#[allow(unknown_lints)]
|
|
#[allow(clippy::zombie_processes)]
|
|
if let Some(name) = &self.guest.test_name {
|
|
ProcessRegistry::spawn(name, &mut self.command)
|
|
} else {
|
|
self.command.spawn()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn args<I, S>(&mut self, args: I) -> &mut Self
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: AsRef<OsStr>,
|
|
{
|
|
self.command.args(args);
|
|
self
|
|
}
|
|
|
|
pub fn default_disks(&mut self) -> &mut Self {
|
|
self.default_disks_inner(true)
|
|
}
|
|
|
|
pub fn default_disks_sparse_off(&mut self) -> &mut Self {
|
|
self.default_disks_inner(false)
|
|
}
|
|
|
|
fn default_disks_inner(&mut self, sparse: bool) -> &mut Self {
|
|
let sparse_opt = if sparse { "" } else { ",sparse=off" };
|
|
let os_disk = format!(
|
|
"path={}{}",
|
|
self.guest
|
|
.disk_config
|
|
.disk(DiskType::OperatingSystem)
|
|
.unwrap(),
|
|
sparse_opt
|
|
);
|
|
if let Some(cloud_init) = self.guest.disk_config.disk(DiskType::CloudInit) {
|
|
self.args([
|
|
"--disk",
|
|
os_disk.as_str(),
|
|
format!("path={cloud_init}").as_str(),
|
|
])
|
|
} else {
|
|
self.args(["--disk", os_disk.as_str()])
|
|
}
|
|
}
|
|
|
|
pub fn default_net(&mut self) -> &mut Self {
|
|
self.args(["--net", self.guest.default_net_string().as_str()])
|
|
}
|
|
|
|
pub fn default_kernel_cmdline_with_platform(&mut self, platform: Option<&str>) -> &mut Self {
|
|
if self.guest.vm_type == GuestVmType::Confidential {
|
|
let console_str = if let Some(c) = &self.guest.console_type {
|
|
c.as_str()
|
|
} else {
|
|
"hvc0"
|
|
};
|
|
let igvm = direct_igvm_boot_path(Some(console_str))
|
|
.expect("IGVM boot file not found for console type: {console_str}");
|
|
self.command.args([
|
|
"--igvm",
|
|
igvm.to_str().expect("IGVM path is not valid UTF-8"),
|
|
]);
|
|
self.command
|
|
.args(["--host-data", generate_host_data().as_str()]);
|
|
self.command.args([
|
|
"--platform",
|
|
&format!(
|
|
"{}sev_snp=on",
|
|
if let Some(p) = platform {
|
|
format!("{p},")
|
|
} else {
|
|
String::new()
|
|
}
|
|
),
|
|
]);
|
|
} else if let Some(kernel) = &self.guest.kernel_path {
|
|
self.command.args(["--kernel", kernel.as_str()]);
|
|
if let Some(cmdline) = &self.guest.kernel_cmdline {
|
|
self.command.args(["--cmdline", cmdline]);
|
|
}
|
|
if let Some(platform_arg) = platform {
|
|
self.command.args(["--platform", platform_arg]);
|
|
}
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
pub fn default_kernel_cmdline(&mut self) -> &mut Self {
|
|
self.default_kernel_cmdline_with_platform(None)
|
|
}
|
|
|
|
pub fn default_cpus(&mut self) -> &mut Self {
|
|
self.args(["--cpus", self.guest.default_cpus_string().as_str()])
|
|
}
|
|
|
|
pub fn default_cpus_with_affinity(&mut self) -> &mut Self {
|
|
// Only support cpu affinity for 2 VCPUs for now,
|
|
// as it is only used in a test that validates cpu affinity is applied correctly.
|
|
assert_eq!(self.guest.num_cpu, 2);
|
|
self.args([
|
|
"--cpus",
|
|
self.guest.default_cpus_with_affinity_string().as_str(),
|
|
])
|
|
}
|
|
|
|
pub fn default_memory(&mut self) -> &mut Self {
|
|
self.args(["--memory", self.guest.default_memory_string().as_str()])
|
|
}
|
|
}
|
|
|
|
/// Returns the absolute path into the workspaces target directory to locate the desired
|
|
/// executable.
|
|
///
|
|
/// # Arguments
|
|
/// - `cmd`: workspace binary, e.g. `ch-remote` or `cloud-hypervisor`
|
|
pub fn clh_command(cmd: &str) -> String {
|
|
let workspace_root = workspace_root();
|
|
let rustc_target = env::var("BUILD_TARGET").unwrap_or("x86_64-unknown-linux-gnu".to_string());
|
|
let target_artifact_dir = format!("target/{rustc_target}/release");
|
|
let target_cmd_path = format!("{target_artifact_dir}/{cmd}");
|
|
|
|
let full_path = workspace_root.join(&target_cmd_path);
|
|
String::from(full_path.to_str().unwrap())
|
|
}
|
|
|
|
pub 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
|
|
}
|
|
}
|
|
|
|
pub fn remote_command_w_output(
|
|
api_socket: &str,
|
|
command: &str,
|
|
arg: Option<&str>,
|
|
) -> (bool, Vec<u8> /* stdout */, Vec<u8> /* stderr */) {
|
|
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, output.stderr)
|
|
}
|
|
|
|
pub fn parse_iperf3_output(output: &[u8], sender: bool, bandwidth: bool) -> Result<f64, Error> {
|
|
std::panic::catch_unwind(|| {
|
|
let s = String::from_utf8_lossy(output);
|
|
let v: Value = serde_json::from_str(&s).expect("'iperf3' parse error: invalid json output");
|
|
|
|
if bandwidth {
|
|
if sender {
|
|
v["end"]["sum_sent"]["bits_per_second"]
|
|
.as_f64()
|
|
.expect("'iperf3' parse error: missing entry 'end.sum_sent.bits_per_second'")
|
|
} else {
|
|
v["end"]["sum_received"]["bits_per_second"].as_f64().expect(
|
|
"'iperf3' parse error: missing entry 'end.sum_received.bits_per_second'",
|
|
)
|
|
}
|
|
} else {
|
|
// iperf does not distinguish sent vs received in this case.
|
|
|
|
let lost_packets = v["end"]["sum"]["lost_packets"]
|
|
.as_f64()
|
|
.expect("'iperf3' parse error: missing entry 'end.sum.lost_packets'");
|
|
let packets = v["end"]["sum"]["packets"]
|
|
.as_f64()
|
|
.expect("'iperf3' parse error: missing entry 'end.sum.packets'");
|
|
let seconds = v["end"]["sum"]["seconds"]
|
|
.as_f64()
|
|
.expect("'iperf3' parse error: missing entry 'end.sum.seconds'");
|
|
|
|
(packets - lost_packets) / seconds
|
|
}
|
|
})
|
|
.map_err(|_| {
|
|
eprintln!(
|
|
"==== Start iperf3 output ===\n\n{}\n\n=== End iperf3 output ===\n\n",
|
|
String::from_utf8_lossy(output)
|
|
);
|
|
Error::Iperf3Parse
|
|
})
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub enum FioOps {
|
|
Read,
|
|
RandomRead,
|
|
Write,
|
|
RandomWrite,
|
|
ReadWrite,
|
|
RandRW,
|
|
}
|
|
|
|
impl fmt::Display for FioOps {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
FioOps::Read => write!(f, "read"),
|
|
FioOps::RandomRead => write!(f, "randread"),
|
|
FioOps::Write => write!(f, "write"),
|
|
FioOps::RandomWrite => write!(f, "randwrite"),
|
|
FioOps::ReadWrite => write!(f, "rw"),
|
|
FioOps::RandRW => write!(f, "randrw"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn parse_fio_output(output: &str, fio_ops: &FioOps, num_jobs: u32) -> Result<f64, Error> {
|
|
std::panic::catch_unwind(|| {
|
|
let v: Value =
|
|
serde_json::from_str(output).expect("'fio' parse error: invalid json output");
|
|
let jobs = v["jobs"]
|
|
.as_array()
|
|
.expect("'fio' parse error: missing entry 'jobs'");
|
|
assert_eq!(
|
|
jobs.len(),
|
|
num_jobs as usize,
|
|
"'fio' parse error: Unexpected number of 'fio' jobs."
|
|
);
|
|
|
|
let (read, write) = match fio_ops {
|
|
FioOps::Read | FioOps::RandomRead => (true, false),
|
|
FioOps::Write | FioOps::RandomWrite => (false, true),
|
|
FioOps::ReadWrite | FioOps::RandRW => (true, true),
|
|
};
|
|
|
|
let mut total_bps = 0_f64;
|
|
for j in jobs {
|
|
if read {
|
|
let bytes = j["read"]["io_bytes"]
|
|
.as_u64()
|
|
.expect("'fio' parse error: missing entry 'read.io_bytes'");
|
|
let runtime = j["read"]["runtime"]
|
|
.as_u64()
|
|
.expect("'fio' parse error: missing entry 'read.runtime'")
|
|
as f64
|
|
/ 1000_f64;
|
|
total_bps += bytes as f64 / runtime;
|
|
}
|
|
if write {
|
|
let bytes = j["write"]["io_bytes"]
|
|
.as_u64()
|
|
.expect("'fio' parse error: missing entry 'write.io_bytes'");
|
|
let runtime = j["write"]["runtime"]
|
|
.as_u64()
|
|
.expect("'fio' parse error: missing entry 'write.runtime'")
|
|
as f64
|
|
/ 1000_f64;
|
|
total_bps += bytes as f64 / runtime;
|
|
}
|
|
}
|
|
|
|
total_bps
|
|
})
|
|
.map_err(|_| {
|
|
eprintln!("=== Start Fio output ===\n\n{output}\n\n=== End Fio output ===\n\n");
|
|
Error::FioOutputParse
|
|
})
|
|
}
|
|
|
|
pub fn parse_fio_output_iops(output: &str, fio_ops: &FioOps, num_jobs: u32) -> Result<f64, Error> {
|
|
std::panic::catch_unwind(|| {
|
|
let v: Value =
|
|
serde_json::from_str(output).expect("'fio' parse error: invalid json output");
|
|
let jobs = v["jobs"]
|
|
.as_array()
|
|
.expect("'fio' parse error: missing entry 'jobs'");
|
|
assert_eq!(
|
|
jobs.len(),
|
|
num_jobs as usize,
|
|
"'fio' parse error: Unexpected number of 'fio' jobs."
|
|
);
|
|
|
|
let (read, write) = match fio_ops {
|
|
FioOps::Read | FioOps::RandomRead => (true, false),
|
|
FioOps::Write | FioOps::RandomWrite => (false, true),
|
|
FioOps::ReadWrite | FioOps::RandRW => (true, true),
|
|
};
|
|
|
|
let mut total_iops = 0_f64;
|
|
for j in jobs {
|
|
if read {
|
|
let ios = j["read"]["total_ios"]
|
|
.as_u64()
|
|
.expect("'fio' parse error: missing entry 'read.total_ios'");
|
|
let runtime = j["read"]["runtime"]
|
|
.as_u64()
|
|
.expect("'fio' parse error: missing entry 'read.runtime'")
|
|
as f64
|
|
/ 1000_f64;
|
|
total_iops += ios as f64 / runtime;
|
|
}
|
|
if write {
|
|
let ios = j["write"]["total_ios"]
|
|
.as_u64()
|
|
.expect("'fio' parse error: missing entry 'write.total_ios'");
|
|
let runtime = j["write"]["runtime"]
|
|
.as_u64()
|
|
.expect("'fio' parse error: missing entry 'write.runtime'")
|
|
as f64
|
|
/ 1000_f64;
|
|
total_iops += ios as f64 / runtime;
|
|
}
|
|
}
|
|
|
|
total_iops
|
|
})
|
|
.map_err(|_| {
|
|
eprintln!("=== Start Fio output ===\n\n{output}\n\n=== End Fio output ===\n\n");
|
|
Error::FioOutputParse
|
|
})
|
|
}
|
|
|
|
// Wait the child process for a given timeout
|
|
fn child_wait_timeout(child: &mut Child, timeout: u64) -> Result<(), WaitTimeoutError> {
|
|
match child.wait_timeout(Duration::from_secs(timeout)) {
|
|
Err(e) => {
|
|
return Err(WaitTimeoutError::General(e));
|
|
}
|
|
Ok(s) => match s {
|
|
None => {
|
|
return Err(WaitTimeoutError::Timedout);
|
|
}
|
|
Some(s) => {
|
|
if !s.success() {
|
|
return Err(WaitTimeoutError::ExitStatus);
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn measure_virtio_net_throughput(
|
|
test_timeout: u32,
|
|
queue_pairs: u32,
|
|
guest: &Guest,
|
|
receive: bool,
|
|
bandwidth: bool,
|
|
) -> Result<f64, Error> {
|
|
let default_port = 5201;
|
|
|
|
// 1. start the iperf3 server on the guest
|
|
for n in 0..queue_pairs {
|
|
guest.ssh_command(&format!("iperf3 -s -p {} -D", default_port + n))?;
|
|
}
|
|
|
|
thread::sleep(Duration::new(1, 0));
|
|
|
|
// 2. start the iperf3 client on host to measure RX through-put
|
|
let mut clients = Vec::new();
|
|
for n in 0..queue_pairs {
|
|
let mut cmd = Command::new("iperf3");
|
|
cmd.args([
|
|
"-J", // Output in JSON format
|
|
"-c",
|
|
&guest.network.guest_ip0,
|
|
"-p",
|
|
&format!("{}", default_port + n),
|
|
"-t",
|
|
&format!("{test_timeout}"),
|
|
"-i",
|
|
"0",
|
|
]);
|
|
// For measuring the guest transmit throughput (as a sender),
|
|
// use reverse mode of the iperf3 client on the host
|
|
if !receive {
|
|
cmd.args(["-R"]);
|
|
}
|
|
// Use UDP stream to measure packets per second. The bitrate is set to
|
|
// 1T to make sure it saturates the link.
|
|
if !bandwidth {
|
|
cmd.args(["-u", "-b", "1T"]);
|
|
}
|
|
cmd.stderr(Stdio::piped()).stdout(Stdio::piped());
|
|
let client = if let Some(name) = &guest.test_name {
|
|
ProcessRegistry::spawn(name, &mut cmd).map_err(Error::Spawn)?
|
|
} else {
|
|
cmd.spawn().map_err(Error::Spawn)?
|
|
};
|
|
|
|
clients.push(client);
|
|
}
|
|
|
|
let mut err: Option<Error> = None;
|
|
let mut results = Vec::new();
|
|
let mut failed = false;
|
|
for c in clients {
|
|
let mut c = c;
|
|
if let Err(e) = child_wait_timeout(&mut c, test_timeout as u64 + 5) {
|
|
err = Some(Error::WaitTimeout(e));
|
|
failed = true;
|
|
}
|
|
|
|
if failed {
|
|
let _ = c.kill();
|
|
let output = c.wait_with_output().unwrap();
|
|
println!(
|
|
"=============== Client output [Error] ===============\n\n{}\n\n===========end============\n\n",
|
|
String::from_utf8_lossy(&output.stdout)
|
|
);
|
|
} else {
|
|
// Safe to unwrap as we know the child has terminated successfully
|
|
let output = c.wait_with_output().unwrap();
|
|
results.push(parse_iperf3_output(&output.stdout, receive, bandwidth)?);
|
|
}
|
|
}
|
|
|
|
if let Some(e) = err {
|
|
Err(e)
|
|
} else {
|
|
Ok(results.iter().sum())
|
|
}
|
|
}
|
|
|
|
pub fn parse_ethr_latency_output(output: &[u8]) -> Result<Vec<f64>, Error> {
|
|
std::panic::catch_unwind(|| {
|
|
let s = String::from_utf8_lossy(output);
|
|
let mut latency = Vec::new();
|
|
for l in s.lines() {
|
|
let v: Value = serde_json::from_str(l).expect("'ethr' parse error: invalid json line");
|
|
// Skip header/summary lines
|
|
if let Some(avg) = v["Avg"].as_str() {
|
|
// Assume the latency unit is always "us"
|
|
latency.push(
|
|
avg.split("us").collect::<Vec<&str>>()[0]
|
|
.parse::<f64>()
|
|
.expect("'ethr' parse error: invalid 'Avg' entry"),
|
|
);
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
!latency.is_empty(),
|
|
"'ethr' parse error: no valid latency data found"
|
|
);
|
|
|
|
latency
|
|
})
|
|
.map_err(|_| {
|
|
eprintln!(
|
|
"=== Start ethr output ===\n\n{}\n\n=== End ethr output ===\n\n",
|
|
String::from_utf8_lossy(output)
|
|
);
|
|
Error::EthrLogParse
|
|
})
|
|
}
|
|
|
|
pub fn measure_virtio_net_latency(guest: &Guest, test_timeout: u32) -> Result<Vec<f64>, Error> {
|
|
// copy the 'ethr' tool to the guest image
|
|
let ethr_path = "/usr/local/bin/ethr";
|
|
let ethr_remote_path = "/tmp/ethr";
|
|
scp_to_guest(
|
|
Path::new(ethr_path),
|
|
Path::new(ethr_remote_path),
|
|
&guest.network.guest_ip0,
|
|
//DEFAULT_SSH_RETRIES,
|
|
1,
|
|
DEFAULT_SSH_TIMEOUT,
|
|
)?;
|
|
|
|
// Start the ethr server on the guest
|
|
guest.ssh_command(&format!("{ethr_remote_path} -s &> /dev/null &"))?;
|
|
|
|
thread::sleep(Duration::new(10, 0));
|
|
|
|
// Start the ethr client on the host
|
|
let log_file = guest
|
|
.tmp_dir
|
|
.as_path()
|
|
.join("ethr.client.log")
|
|
.to_str()
|
|
.unwrap()
|
|
.to_string();
|
|
let mut c = {
|
|
let mut cmd = Command::new(ethr_path);
|
|
cmd.args([
|
|
"-c",
|
|
&guest.network.guest_ip0,
|
|
"-t",
|
|
"l",
|
|
"-o",
|
|
&log_file, // file output is JSON format
|
|
"-d",
|
|
&format!("{test_timeout}s"),
|
|
])
|
|
.stderr(Stdio::piped())
|
|
.stdout(Stdio::piped());
|
|
if let Some(name) = &guest.test_name {
|
|
ProcessRegistry::spawn(name, &mut cmd).map_err(Error::Spawn)?
|
|
} else {
|
|
cmd.spawn().map_err(Error::Spawn)?
|
|
}
|
|
};
|
|
|
|
if let Err(e) = child_wait_timeout(&mut c, test_timeout as u64 + 5).map_err(Error::WaitTimeout)
|
|
{
|
|
let _ = c.kill();
|
|
return Err(e);
|
|
}
|
|
|
|
// Parse the ethr latency test output
|
|
let content = fs::read(log_file).map_err(Error::EthrLogFile)?;
|
|
parse_ethr_latency_output(&content)
|
|
}
|
|
|
|
// parse the bar address from the output of `lspci -vv`
|
|
|
|
pub fn extract_bar_address(output: &str, device_desc: &str, bar_index: usize) -> Option<String> {
|
|
let devices: Vec<&str> = output.split("\n\n").collect();
|
|
|
|
for device in devices {
|
|
if device.contains(device_desc) {
|
|
for line in device.lines() {
|
|
let line = line.trim();
|
|
let line_start_str = format!("Region {bar_index}: Memory at");
|
|
// for example: Region 2: Memory at 200000000 (64-bit, non-prefetchable) [size=1M]
|
|
if line.starts_with(line_start_str.as_str()) {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() >= 4 {
|
|
let addr_str = parts[4];
|
|
return Some(String::from(addr_str));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Copy, Default)]
|
|
pub enum GuestVmType {
|
|
#[default]
|
|
Regular,
|
|
Confidential,
|
|
}
|
|
|
|
impl FromStr for GuestVmType {
|
|
type Err = ();
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"regular" => Ok(GuestVmType::Regular),
|
|
"confidential" => Ok(GuestVmType::Confidential),
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for GuestVmType {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
match self {
|
|
GuestVmType::Regular => write!(f, "regular"),
|
|
GuestVmType::Confidential => write!(f, "confidential"),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the direct igvm boot file path based on the console type
|
|
fn direct_igvm_boot_path(console: Option<&str>) -> Option<PathBuf> {
|
|
// get the default hvc0 igvm file if console string is not passed
|
|
let console_str = console.unwrap_or("hvc0");
|
|
|
|
if console_str != "hvc0" && console_str != "ttyS0" {
|
|
panic!("IGVM console should be hvc0 or ttyS0, got: {console_str}");
|
|
}
|
|
|
|
let igvm_filepath = format!("/igvm_files/linux-{console_str}.bin");
|
|
if Path::new(&igvm_filepath).exists() {
|
|
Some(PathBuf::from(igvm_filepath))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
// Generate a random 64-character hex string for host data
|
|
fn generate_host_data() -> String {
|
|
let mut bytes = [0u8; 32];
|
|
rand::rng().fill_bytes(&mut bytes);
|
|
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
|
}
|
|
|
|
// 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.
|
|
pub 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-x86_64");
|
|
#[cfg(target_arch = "aarch64")]
|
|
kernel_path.push("Image-arm64");
|
|
|
|
kernel_path
|
|
}
|
|
|
|
pub 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
|
|
}
|
|
|
|
pub const DIRECT_KERNEL_BOOT_CMDLINE: &str =
|
|
"root=/dev/vda1 console=hvc0 rw systemd.journald.forward_to_console=1";
|
|
|
|
pub const CONSOLE_TEST_STRING: &str = "Started OpenBSD Secure Shell server";
|
|
|
|
// Constant taken from the VMM crate.
|
|
pub const MAX_NUM_PCI_SEGMENTS: u16 = 96;
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
pub mod x86_64 {
|
|
pub const JAMMY_VFIO_IMAGE_NAME: &str =
|
|
"jammy-server-cloudimg-amd64-custom-vfio-20241012-0.raw";
|
|
pub const JAMMY_IMAGE_NAME: &str = "jammy-server-cloudimg-amd64-custom-20241017-0.raw";
|
|
pub const JAMMY_IMAGE_NAME_VHD: &str = "jammy-server-cloudimg-amd64-custom-20241017-0.vhd";
|
|
pub const JAMMY_IMAGE_NAME_VHDX: &str = "jammy-server-cloudimg-amd64-custom-20241017-0.vhdx";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2: &str = "jammy-server-cloudimg-amd64-custom-20241017-0.qcow2";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2_ZLIB: &str =
|
|
"jammy-server-cloudimg-amd64-custom-20241017-0-zlib.qcow2";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2_ZSTD: &str =
|
|
"jammy-server-cloudimg-amd64-custom-20241017-0-zstd.qcow2";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2_BACKING_ZSTD_FILE: &str =
|
|
"jammy-server-cloudimg-amd64-custom-20241017-0-backing-zstd.qcow2";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2_BACKING_UNCOMPRESSED_FILE: &str =
|
|
"jammy-server-cloudimg-amd64-custom-20241017-0-backing-uncompressed.qcow2";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2_BACKING_RAW_FILE: &str =
|
|
"jammy-server-cloudimg-amd64-custom-20241017-0-backing-raw.qcow2";
|
|
pub const WINDOWS_IMAGE_NAME: &str = "windows-server-2025-amd64-1.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")]
|
|
pub use x86_64::*;
|
|
|
|
#[cfg(target_arch = "aarch64")]
|
|
pub mod aarch64 {
|
|
pub const JAMMY_IMAGE_NAME: &str = "jammy-server-cloudimg-arm64-custom-20220329-0.raw";
|
|
pub const JAMMY_IMAGE_NAME_VHD: &str = "jammy-server-cloudimg-arm64-custom-20220329-0.vhd";
|
|
pub const JAMMY_IMAGE_NAME_VHDX: &str = "jammy-server-cloudimg-arm64-custom-20220329-0.vhdx";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2: &str = "jammy-server-cloudimg-arm64-custom-20220329-0.qcow2";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2_ZLIB: &str =
|
|
"jammy-server-cloudimg-arm64-custom-20220329-0-zlib.qcow2";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2_ZSTD: &str =
|
|
"jammy-server-cloudimg-arm64-custom-20220329-0-zstd.qcow2";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2_BACKING_ZSTD_FILE: &str =
|
|
"jammy-server-cloudimg-arm64-custom-20220329-0-backing-zstd.qcow2";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2_BACKING_UNCOMPRESSED_FILE: &str =
|
|
"jammy-server-cloudimg-arm64-custom-20220329-0-backing-uncompressed.qcow2";
|
|
pub const JAMMY_IMAGE_NAME_QCOW2_BACKING_RAW_FILE: &str =
|
|
"jammy-server-cloudimg-arm64-custom-20220329-0-backing-raw.qcow2";
|
|
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")]
|
|
pub use aarch64::*;
|
|
|
|
#[cfg(test)]
|
|
mod unit_tests {
|
|
use super::*;
|
|
|
|
fn is_alive(pid: u32) -> bool {
|
|
unsafe { libc::kill(pid as i32, 0) == 0 }
|
|
}
|
|
|
|
#[test]
|
|
fn process_registry_spawn_and_cleanup() {
|
|
let name = "test_spawn_and_cleanup";
|
|
let mut cmd = Command::new("sleep");
|
|
cmd.arg("60");
|
|
let mut child = ProcessRegistry::spawn(name, &mut cmd).unwrap();
|
|
let pid = child.id();
|
|
|
|
assert!(is_alive(pid));
|
|
assert!(ProcessRegistry::cleanup(name));
|
|
let _ = child.wait();
|
|
assert!(!is_alive(pid));
|
|
}
|
|
|
|
#[test]
|
|
fn process_registry_shared_group() {
|
|
let name = "test_shared_group";
|
|
|
|
let mut cmd1 = Command::new("sleep");
|
|
cmd1.arg("60");
|
|
let mut child1 = ProcessRegistry::spawn(name, &mut cmd1).unwrap();
|
|
let pid1 = child1.id();
|
|
|
|
let mut cmd2 = Command::new("sleep");
|
|
cmd2.arg("60");
|
|
let mut child2 = ProcessRegistry::spawn(name, &mut cmd2).unwrap();
|
|
let pid2 = child2.id();
|
|
|
|
let pgid1 = unsafe { libc::getpgid(pid1 as i32) };
|
|
let pgid2 = unsafe { libc::getpgid(pid2 as i32) };
|
|
assert_eq!(pgid1, pgid2);
|
|
assert_eq!(pgid1, pid1 as i32);
|
|
|
|
assert!(ProcessRegistry::cleanup(name));
|
|
let _ = child1.wait();
|
|
let _ = child2.wait();
|
|
assert!(!is_alive(pid1));
|
|
assert!(!is_alive(pid2));
|
|
}
|
|
|
|
#[test]
|
|
fn process_registry_cleanup_unknown() {
|
|
assert!(!ProcessRegistry::cleanup("nonexistent"));
|
|
}
|
|
|
|
#[test]
|
|
fn process_registry_stale_group_replaced() {
|
|
let name = "test_stale_group";
|
|
|
|
let mut cmd1 = Command::new("sleep");
|
|
cmd1.arg("0");
|
|
let mut child1 = ProcessRegistry::spawn(name, &mut cmd1).unwrap();
|
|
let pid1 = child1.id();
|
|
let _ = child1.wait();
|
|
|
|
// Group is now stale. Next spawn should create a fresh group.
|
|
let mut cmd2 = Command::new("sleep");
|
|
cmd2.arg("60");
|
|
let mut child2 = ProcessRegistry::spawn(name, &mut cmd2).unwrap();
|
|
let pid2 = child2.id();
|
|
|
|
assert_ne!(pid1, pid2);
|
|
let pgid2 = unsafe { libc::getpgid(pid2 as i32) };
|
|
assert_eq!(pgid2, pid2 as i32);
|
|
|
|
assert!(ProcessRegistry::cleanup(name));
|
|
let _ = child2.wait();
|
|
assert!(!is_alive(pid2));
|
|
}
|
|
}
|