tests: refactor test_api* to support the D-Bus API and add a new test

Implemented a `TargetApi` enum to make the process of implementing
tests for the D-Bus and HTTP API more convenient.

Refactored `test_api_{create_boot, shutdown, pause_resume, delete}` tests
with the `TargetApi` enum to also implement tests for the D-Bus API.

Added a new test named `test_api_dbus_and_http_interleaved` that uses
both the HTTP and D-Bus API at the same time.

Modified integration test scripts to enable the `dbus_api` feature when
compiling and start a dbus-session when integration tests are run.

Signed-off-by: Omer Faruk Bayram <omer.faruk@sartura.hr>
This commit is contained in:
Omer Faruk Bayram 2023-03-30 23:47:21 +03:00 committed by Bo Chen
parent a64d27f841
commit a7aecb5eee
4 changed files with 250 additions and 72 deletions

View File

@ -426,7 +426,7 @@ cmd_tests() {
--env USER="root" \ --env USER="root" \
--env CH_LIBC="${libc}" \ --env CH_LIBC="${libc}" \
"$CTR_IMAGE" \ "$CTR_IMAGE" \
./scripts/run_integration_tests_"$(uname -m)".sh "$@" || fix_dir_perms $? || exit $? dbus-run-session ./scripts/run_integration_tests_"$(uname -m)".sh "$@" || fix_dir_perms $? || exit $?
fi fi
if [ "$integration_sgx" = true ]; then if [ "$integration_sgx" = true ]; then

View File

@ -231,7 +231,7 @@ fi
export RUST_BACKTRACE=1 export RUST_BACKTRACE=1
cargo build --all --release --target $BUILD_TARGET cargo build --features "dbus_api" --all --release --target $BUILD_TARGET
# Enable KSM with some reasonable parameters so that it won't take too long # Enable KSM with some reasonable parameters so that it won't take too long
# for the memory to be merged between two processes. # for the memory to be merged between two processes.

View File

@ -12,10 +12,10 @@ mkdir -p "$WORKLOADS_DIR"
process_common_args "$@" process_common_args "$@"
# For now these values are default for kvm # For now these values are default for kvm
test_features="" test_features="--features dbus_api"
if [ "$hypervisor" = "mshv" ] ; then if [ "$hypervisor" = "mshv" ] ; then
test_features="--no-default-features --features mshv" test_features="--no-default-features ${test_features},mshv"
fi fi
cp scripts/sha1sums-x86_64 $WORKLOADS_DIR cp scripts/sha1sums-x86_64 $WORKLOADS_DIR
@ -156,7 +156,7 @@ cp $VMLINUX_IMAGE $VFIO_DIR || exit 1
BUILD_TARGET="$(uname -m)-unknown-linux-${CH_LIBC}" BUILD_TARGET="$(uname -m)-unknown-linux-${CH_LIBC}"
cargo build --no-default-features --features "kvm,mshv" --all --release --target $BUILD_TARGET cargo build --no-default-features --features "kvm,mshv,dbus_api" --all --release --target $BUILD_TARGET
# We always copy a fresh version of our binary for our L2 guest. # We always copy a fresh version of our binary for our L2 guest.
cp target/$BUILD_TARGET/release/cloud-hypervisor $VFIO_DIR cp target/$BUILD_TARGET/release/cloud-hypervisor $VFIO_DIR

View File

@ -10,8 +10,6 @@
extern crate test_infra; extern crate test_infra;
use api_client::simple_api_command;
use api_client::simple_api_full_command;
use net_util::MacAddr; use net_util::MacAddr;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
@ -75,6 +73,73 @@ const DIRECT_KERNEL_BOOT_CMDLINE: &str =
const CONSOLE_TEST_STRING: &str = "Started OpenBSD Secure Shell server"; 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<&str> {
match self {
TargetApi::HttpApi(api_socket) => {
vec!["--api-socket", api_socket.as_str()]
}
TargetApi::DBusApi(service_name, object_path) => {
vec![
"--dbus-service-name",
service_name.as_str(),
"--dbus-object-path",
object_path.as_str(),
]
}
}
}
fn remote_args(&self) -> Vec<&str> {
// `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
}
}
}
fn prepare_virtiofsd(tmp_dir: &TempDir, shared_dir: &str) -> (std::process::Child, String) { fn prepare_virtiofsd(tmp_dir: &TempDir, shared_dir: &str) -> (std::process::Child, String) {
let mut workload_path = dirs::home_dir().unwrap(); let mut workload_path = dirs::home_dir().unwrap();
workload_path.push("workloads"); workload_path.push("workloads");
@ -1910,7 +1975,7 @@ fn enable_guest_watchdog(guest: &Guest, watchdog_sec: u32) {
} }
mod common_parallel { mod common_parallel {
use std::{fs::OpenOptions, io::SeekFrom, os::unix::net::UnixStream}; use std::{fs::OpenOptions, io::SeekFrom};
use crate::*; use crate::*;
@ -4037,46 +4102,126 @@ mod common_parallel {
_test_virtio_vsock(true); _test_virtio_vsock(true);
} }
// Start cloud-hypervisor with no VM parameters, running both the HTTP
// and DBus APIs. Alternate calls to the external APIs (HTTP and DBus)
// to create a VM, boot it, and verify that it can be shut down and then
// booted again.
#[test] #[test]
// Start cloud-hypervisor with no VM parameters, only the API server running. fn test_api_dbus_and_http_interleaved() {
// From the API: Create a VM, boot it and check that it looks as expected.
fn test_api_create_boot() {
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(focal)); let guest = Guest::new(Box::new(focal));
let dbus_api = TargetApi::new_dbus_api(&guest.tmp_dir);
let api_socket = temp_api_path(&guest.tmp_dir); let http_api = TargetApi::new_http_api(&guest.tmp_dir);
let mut child = GuestCommand::new(&guest) let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket]) .args(dbus_api.guest_args())
.args(http_api.guest_args())
.capture_output() .capture_output()
.spawn() .spawn()
.unwrap(); .unwrap();
thread::sleep(std::time::Duration::new(1, 0)); thread::sleep(std::time::Duration::new(1, 0));
let mut socket = UnixStream::connect(&api_socket).unwrap(); // Verify API servers are running
// Verify API server is running assert!(dbus_api.remote_command("ping", None));
simple_api_full_command(&mut socket, "GET", "vmm.ping", None).unwrap(); assert!(http_api.remote_command("ping", None));
// Create the VM first // Create the VM first
let cpu_count: u8 = 4; let cpu_count: u8 = 4;
let http_body = guest.api_create_body( let request_body = guest.api_create_body(
cpu_count, cpu_count,
direct_kernel_boot_path().to_str().unwrap(), direct_kernel_boot_path().to_str().unwrap(),
DIRECT_KERNEL_BOOT_CMDLINE, DIRECT_KERNEL_BOOT_CMDLINE,
); );
let temp_config_path = guest.tmp_dir.as_path().join("config"); let temp_config_path = guest.tmp_dir.as_path().join("config");
std::fs::write(&temp_config_path, http_body).unwrap(); std::fs::write(&temp_config_path, request_body).unwrap();
let create_config = temp_config_path.as_os_str().to_str().unwrap();
remote_command( let r = std::panic::catch_unwind(|| {
&api_socket, // Create the VM
"create", assert!(dbus_api.remote_command("create", Some(create_config),));
Some(temp_config_path.as_os_str().to_str().unwrap()),
);
// Then boot it // Then boot it
remote_command(&api_socket, "boot", None); assert!(http_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 shutdown the VM
assert!(dbus_api.remote_command("shutdown", None));
// Then boot it again
assert!(http_api.remote_command("boot", None));
guest.wait_vm_boot(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);
});
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
}
#[test]
fn test_api_dbus_create_boot() {
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(focal));
_test_api_create_boot(TargetApi::new_dbus_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)
}
// 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)); thread::sleep(std::time::Duration::new(20, 0));
let r = std::panic::catch_unwind(|| { let r = std::panic::catch_unwind(|| {
@ -4092,42 +4237,53 @@ mod common_parallel {
} }
#[test] #[test]
// Start cloud-hypervisor with no VM parameters, only the API server running. fn test_api_dbus_shutdown() {
// From the API: Create a VM, boot it and check it can be shutdown and then
// booted again
fn test_api_shutdown() {
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(focal)); let guest = Guest::new(Box::new(focal));
let api_socket = temp_api_path(&guest.tmp_dir); _test_api_shutdown(TargetApi::new_dbus_api(&guest.tmp_dir), guest)
}
#[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)
}
// 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) let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket]) .args(target_api.guest_args())
.capture_output() .capture_output()
.spawn() .spawn()
.unwrap(); .unwrap();
thread::sleep(std::time::Duration::new(1, 0)); thread::sleep(std::time::Duration::new(1, 0));
let mut socket = UnixStream::connect(&api_socket).unwrap();
// Verify API server is running // Verify API server is running
simple_api_full_command(&mut socket, "GET", "vmm.ping", None).unwrap(); assert!(target_api.remote_command("ping", None));
// Create the VM first // Create the VM first
let cpu_count: u8 = 4; let cpu_count: u8 = 4;
let http_body = guest.api_create_body( let request_body = guest.api_create_body(
cpu_count, cpu_count,
direct_kernel_boot_path().to_str().unwrap(), direct_kernel_boot_path().to_str().unwrap(),
DIRECT_KERNEL_BOOT_CMDLINE, 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(|| { let r = std::panic::catch_unwind(|| {
// socket has to be created again inside catch_unwind block to avoid errors assert!(target_api.remote_command("create", Some(create_config)));
let mut socket = UnixStream::connect(&api_socket).unwrap();
simple_api_command(&mut socket, "PUT", "create", Some(&http_body)).unwrap();
// Then boot it // Then boot it
simple_api_command(&mut socket, "PUT", "boot", None).unwrap(); assert!(target_api.remote_command("boot", None));
guest.wait_vm_boot(None).unwrap(); guest.wait_vm_boot(None).unwrap();
@ -4144,10 +4300,10 @@ mod common_parallel {
thread::sleep(std::time::Duration::new(20, 0)); thread::sleep(std::time::Duration::new(20, 0));
// Then shut it down // Then shut it down
simple_api_command(&mut socket, "PUT", "shutdown", None).unwrap(); assert!(target_api.remote_command("shutdown", None));
// Then boot it again // Then boot it again
simple_api_command(&mut socket, "PUT", "boot", None).unwrap(); assert!(target_api.remote_command("boot", None));
guest.wait_vm_boot(None).unwrap(); guest.wait_vm_boot(None).unwrap();
@ -4163,42 +4319,52 @@ mod common_parallel {
} }
#[test] #[test]
// Start cloud-hypervisor with no VM parameters, only the API server running. fn test_api_dbus_delete() {
// From the API: Create a VM, boot it and check it can be deleted and then recreated
// booted again.
fn test_api_delete() {
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(focal)); let guest = Guest::new(Box::new(focal));
let api_socket = temp_api_path(&guest.tmp_dir); _test_api_delete(TargetApi::new_dbus_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);
}
// 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) let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket]) .args(target_api.guest_args())
.capture_output() .capture_output()
.spawn() .spawn()
.unwrap(); .unwrap();
thread::sleep(std::time::Duration::new(1, 0)); thread::sleep(std::time::Duration::new(1, 0));
let mut socket = UnixStream::connect(&api_socket).unwrap();
// Verify API server is running // Verify API server is running
simple_api_full_command(&mut socket, "GET", "vmm.ping", None).unwrap(); assert!(target_api.remote_command("ping", None));
// Create the VM first // Create the VM first
let cpu_count: u8 = 4; let cpu_count: u8 = 4;
let http_body = guest.api_create_body( let request_body = guest.api_create_body(
cpu_count, cpu_count,
direct_kernel_boot_path().to_str().unwrap(), direct_kernel_boot_path().to_str().unwrap(),
DIRECT_KERNEL_BOOT_CMDLINE, 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(|| { let r = std::panic::catch_unwind(|| {
// socket has to be created again inside catch_unwind block to avoid errors assert!(target_api.remote_command("create", Some(create_config)));
let mut socket = UnixStream::connect(&api_socket).unwrap();
simple_api_command(&mut socket, "PUT", "create", Some(&http_body)).unwrap();
// Then boot it // Then boot it
simple_api_command(&mut socket, "PUT", "boot", None).unwrap(); assert!(target_api.remote_command("boot", None));
guest.wait_vm_boot(None).unwrap(); guest.wait_vm_boot(None).unwrap();
@ -4215,12 +4381,12 @@ mod common_parallel {
thread::sleep(std::time::Duration::new(20, 0)); thread::sleep(std::time::Duration::new(20, 0));
// Then delete it // Then delete it
simple_api_command(&mut socket, "PUT", "delete", None).unwrap(); assert!(target_api.remote_command("delete", None));
simple_api_command(&mut socket, "PUT", "create", Some(&http_body)).unwrap(); assert!(target_api.remote_command("create", Some(create_config)));
// Then boot it again // Then boot it again
simple_api_command(&mut socket, "PUT", "boot", None).unwrap(); assert!(target_api.remote_command("boot", None));
guest.wait_vm_boot(None).unwrap(); guest.wait_vm_boot(None).unwrap();
@ -4236,41 +4402,53 @@ mod common_parallel {
} }
#[test] #[test]
fn test_api_dbus_pause_resume() {
let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(focal));
_test_api_pause_resume(TargetApi::new_dbus_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)
}
// Start cloud-hypervisor with no VM parameters, only the API server running. // 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. // 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. // Then we pause the VM, check that it's no longer available.
// Finally we resume the VM and check that it's available. // Finally we resume the VM and check that it's available.
fn test_api_pause_resume() { fn _test_api_pause_resume(target_api: TargetApi, guest: Guest) {
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 mut child = GuestCommand::new(&guest) let mut child = GuestCommand::new(&guest)
.args(["--api-socket", &api_socket]) .args(target_api.guest_args())
.capture_output() .capture_output()
.spawn() .spawn()
.unwrap(); .unwrap();
thread::sleep(std::time::Duration::new(1, 0)); thread::sleep(std::time::Duration::new(1, 0));
let mut socket = UnixStream::connect(&api_socket).unwrap();
// Verify API server is running // Verify API server is running
simple_api_full_command(&mut socket, "GET", "vmm.ping", None).unwrap(); assert!(target_api.remote_command("ping", None));
// Create the VM first // Create the VM first
let cpu_count: u8 = 4; let cpu_count: u8 = 4;
let http_body = guest.api_create_body( let request_body = guest.api_create_body(
cpu_count, cpu_count,
direct_kernel_boot_path().to_str().unwrap(), direct_kernel_boot_path().to_str().unwrap(),
DIRECT_KERNEL_BOOT_CMDLINE, DIRECT_KERNEL_BOOT_CMDLINE,
); );
simple_api_command(&mut socket, "PUT", "create", Some(&http_body)).unwrap(); 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 // Then boot it
simple_api_command(&mut socket, "PUT", "boot", None).unwrap(); assert!(target_api.remote_command("boot", None));
thread::sleep(std::time::Duration::new(20, 0)); thread::sleep(std::time::Duration::new(20, 0));
let r = std::panic::catch_unwind(|| { let r = std::panic::catch_unwind(|| {
@ -4279,10 +4457,10 @@ mod common_parallel {
assert!(guest.get_total_memory().unwrap_or_default() > 480_000); assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
// We now pause the VM // We now pause the VM
assert!(remote_command(&api_socket, "pause", None)); assert!(target_api.remote_command("pause", None));
// Check pausing again fails // Check pausing again fails
assert!(!remote_command(&api_socket, "pause", None)); assert!(!target_api.remote_command("pause", None));
thread::sleep(std::time::Duration::new(2, 0)); thread::sleep(std::time::Duration::new(2, 0));
@ -4296,10 +4474,10 @@ mod common_parallel {
.is_err()); .is_err());
// Resume the VM // Resume the VM
assert!(remote_command(&api_socket, "resume", None)); assert!(target_api.remote_command("resume", None));
// Check resuming again fails // Check resuming again fails
assert!(!remote_command(&api_socket, "resume", None)); assert!(!target_api.remote_command("resume", None));
thread::sleep(std::time::Duration::new(2, 0)); thread::sleep(std::time::Duration::new(2, 0));