mirror of
https://github.com/kata-containers/kata-containers.git
synced 2025-08-22 17:59:31 +00:00
agent-ctl: Add option --vm to boot pod VM for testing.
This change introduces a new command line option `--vm` to boot up a pod VM for testing. The tool connects with kata agent running inside the VM to send the test commands. The tool uses `hypervisor` crates from runtime-rs for VM lifecycle management. Current implementation supports Qemu & Cloud Hypervisor as VMMs. In summary: - tool parses the VMM specific runtime-rs kata config file in /opt/kata/share/defaults/kata-containers/runtime-rs/* - prepares and starts a VM using runtime-rs::hypervisor vm APIs - retrieves agent's server address to setup connection - tests the requested commands & shutdown the VM Fixes #11566 Signed-off-by: Sumedh Alok Sharma <sumsharma@microsoft.com>
This commit is contained in:
parent
e451e3dcd0
commit
0398073c55
629
src/tools/agent-ctl/Cargo.lock
generated
629
src/tools/agent-ctl/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -30,7 +30,7 @@ rand = "0.8.4"
|
|||||||
protobuf = "3.2.0"
|
protobuf = "3.2.0"
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
|
|
||||||
nix = "0.23.0"
|
nix = "0.24.2"
|
||||||
libc = "0.2.112"
|
libc = "0.2.112"
|
||||||
# XXX: Must be the same as the version used by the agent
|
# XXX: Must be the same as the version used by the agent
|
||||||
ttrpc = "0.8.4"
|
ttrpc = "0.8.4"
|
||||||
@ -49,6 +49,11 @@ image-rs = { git = "https://github.com/confidential-containers/guest-components"
|
|||||||
"signature-cosign-rustls",
|
"signature-cosign-rustls",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
kata-types = { path = "../../libs/kata-types" }
|
||||||
|
|
||||||
|
# hypervisor crate from runtime-rs
|
||||||
|
hypervisor = { path = "../../runtime-rs/crates/hypervisor", features = ["cloud-hypervisor"]}
|
||||||
|
|
||||||
safe-path = { path = "../../libs/safe-path" }
|
safe-path = { path = "../../libs/safe-path" }
|
||||||
|
|
||||||
tokio = { version = "1.44.2", features = ["signal"] }
|
tokio = { version = "1.44.2", features = ["signal"] }
|
||||||
|
@ -7,14 +7,15 @@
|
|||||||
|
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
|
use crate::vm;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use byteorder::ByteOrder;
|
use byteorder::ByteOrder;
|
||||||
use nix::sys::socket::{connect, socket, AddressFamily, SockAddr, SockFlag, SockType, UnixAddr};
|
use nix::sys::socket::{connect, socket, AddressFamily, SockFlag, SockType, UnixAddr, VsockAddr};
|
||||||
use protocols::agent::*;
|
use protocols::agent::*;
|
||||||
use protocols::agent_ttrpc::*;
|
use protocols::agent_ttrpc::*;
|
||||||
use protocols::health::*;
|
use protocols::health::*;
|
||||||
use protocols::health_ttrpc::*;
|
use protocols::health_ttrpc::*;
|
||||||
use slog::{debug, info};
|
use slog::{debug, info, warn};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Write; // XXX: for flush()
|
use std::io::Write; // XXX: for flush()
|
||||||
@ -107,6 +108,12 @@ const METADATA_CFG_NS: &str = "agent-ctl-cfg";
|
|||||||
// automatically.
|
// automatically.
|
||||||
const AUTO_VALUES_CFG_NAME: &str = "auto-values";
|
const AUTO_VALUES_CFG_NAME: &str = "auto-values";
|
||||||
|
|
||||||
|
// Retry count and dial timeout to try connecting to the agent
|
||||||
|
// Static value taken from runtime-rs configuration calculation
|
||||||
|
// # Retry times = reconnect_timeout_ms / dial_timeout_ms (default: 300)
|
||||||
|
const RETRY_AGENT_CONNECT: u64 = 300;
|
||||||
|
const DIAL_TIMEOUT: u64 = 10;
|
||||||
|
|
||||||
static AGENT_CMDS: &[AgentCmd] = &[
|
static AGENT_CMDS: &[AgentCmd] = &[
|
||||||
AgentCmd {
|
AgentCmd {
|
||||||
name: "AddARPNeighbors",
|
name: "AddARPNeighbors",
|
||||||
@ -403,25 +410,43 @@ fn get_builtin_cmd_func(name: &str) -> Result<BuiltinCmdFp> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn client_create_vsock_fd(cid: libc::c_uint, port: u32) -> Result<RawFd> {
|
fn client_create_vsock_fd(cid: libc::c_uint, port: u32) -> Result<RawFd> {
|
||||||
let fd = socket(
|
let sock_addr = VsockAddr::new(cid, port);
|
||||||
AddressFamily::Vsock,
|
|
||||||
SockType::Stream,
|
|
||||||
SockFlag::SOCK_CLOEXEC,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.map_err(|e| anyhow!(e))?;
|
|
||||||
|
|
||||||
let sock_addr = SockAddr::new_vsock(cid, port);
|
for i in 0..RETRY_AGENT_CONNECT {
|
||||||
|
let fd = socket(
|
||||||
|
AddressFamily::Vsock,
|
||||||
|
SockType::Stream,
|
||||||
|
SockFlag::SOCK_CLOEXEC,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow!(e))?;
|
||||||
|
|
||||||
connect(fd, &sock_addr).map_err(|e| anyhow!(e))?;
|
// Connect the socket to vsock server.
|
||||||
|
match connect(fd, &sock_addr) {
|
||||||
|
Ok(_) => return Ok(fd),
|
||||||
|
Err(e) => {
|
||||||
|
debug!(
|
||||||
|
sl!(),
|
||||||
|
"Failed to connect to vsock in attempt:{} error:{:?}", i, e
|
||||||
|
);
|
||||||
|
sleep(Duration::from_millis(DIAL_TIMEOUT));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(fd)
|
Err(anyhow!("Failed to establish vsock connection with agent"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup the existing stream by making a Hybrid VSOCK host-initiated
|
// Setup the existing stream by making a Hybrid VSOCK host-initiated
|
||||||
// connection request to the Hybrid VSOCK-capable hypervisor (CLH or FC),
|
// connection request to the Hybrid VSOCK-capable hypervisor (CLH or FC),
|
||||||
// asking it to route the connection to the Kata Agent running inside the VM.
|
// asking it to route the connection to the Kata Agent running inside the VM.
|
||||||
fn setup_hybrid_vsock(mut stream: &UnixStream, hybrid_vsock_port: u64) -> Result<()> {
|
fn setup_hybrid_vsock(path: &str, hybrid_vsock_port: u64) -> Result<UnixStream> {
|
||||||
|
debug!(
|
||||||
|
sl!(),
|
||||||
|
"setup_hybrid_vsock path:{} port: {}", path, hybrid_vsock_port
|
||||||
|
);
|
||||||
|
|
||||||
// Challenge message sent to the Hybrid VSOCK capable hypervisor asking
|
// Challenge message sent to the Hybrid VSOCK capable hypervisor asking
|
||||||
// for a connection to a real VSOCK server running in the VM on the
|
// for a connection to a real VSOCK server running in the VM on the
|
||||||
// port specified as part of this message.
|
// port specified as part of this message.
|
||||||
@ -431,39 +456,39 @@ fn setup_hybrid_vsock(mut stream: &UnixStream, hybrid_vsock_port: u64) -> Result
|
|||||||
// hypervisor informing the client that the CONNECT_CMD was successful.
|
// hypervisor informing the client that the CONNECT_CMD was successful.
|
||||||
const OK_CMD: &str = "OK";
|
const OK_CMD: &str = "OK";
|
||||||
|
|
||||||
// Contact the agent by dialing it's port number and
|
for i in 0..RETRY_AGENT_CONNECT {
|
||||||
// waiting for the hybrid vsock hypervisor to route the call for us ;)
|
let mut stream = UnixStream::connect(path)?;
|
||||||
//
|
// Contact the agent by dialing it's port number and
|
||||||
// See: https://github.com/firecracker-microvm/firecracker/blob/main/docs/vsock.md#host-initiated-connections
|
// waiting for the hybrid vsock hypervisor to route the call for us ;)
|
||||||
let msg = format!("{} {}\n", CONNECT_CMD, hybrid_vsock_port);
|
//
|
||||||
|
// See: https://github.com/firecracker-microvm/firecracker/blob/main/docs/vsock.md#host-initiated-connections
|
||||||
|
let msg = format!("{} {}\n", CONNECT_CMD, hybrid_vsock_port);
|
||||||
|
stream.write_all(msg.as_bytes())?;
|
||||||
|
|
||||||
stream.write_all(msg.as_bytes())?;
|
// Now, see if we get the expected response
|
||||||
|
let mut reader = BufReader::new(&mut stream);
|
||||||
|
|
||||||
// Now, see if we get the expected response
|
let mut msg = String::new();
|
||||||
let stream_reader = stream.try_clone()?;
|
reader.read_line(&mut msg)?;
|
||||||
let mut reader = BufReader::new(&stream_reader);
|
|
||||||
|
|
||||||
let mut msg = String::new();
|
if msg.starts_with(OK_CMD) {
|
||||||
reader.read_line(&mut msg)?;
|
let response = msg
|
||||||
|
.strip_prefix(OK_CMD)
|
||||||
|
.ok_or(format!("invalid response: {:?}", msg))
|
||||||
|
.map_err(|e| anyhow!(e))?
|
||||||
|
.trim();
|
||||||
|
|
||||||
if msg.starts_with(OK_CMD) {
|
// The Unix stream is now connected directly to the VSOCK socket
|
||||||
let response = msg
|
// the Kata agent is listening to in the VM.
|
||||||
.strip_prefix(OK_CMD)
|
debug!(sl!(), "Hybrid VSOCK host-side port: {:?}", response);
|
||||||
.ok_or(format!("invalid response: {:?}", msg))
|
return Ok(stream);
|
||||||
.map_err(|e| anyhow!(e))?
|
} else {
|
||||||
.trim();
|
debug!(sl!(), "attempt:{} message: {:?}", i, msg);
|
||||||
|
sleep(Duration::from_millis(DIAL_TIMEOUT));
|
||||||
debug!(sl!(), "Hybrid VSOCK host-side port: {:?}", response);
|
continue;
|
||||||
} else {
|
}
|
||||||
return Err(anyhow!(
|
|
||||||
"failed to setup Hybrid VSOCK connection: response was: {:?}",
|
|
||||||
msg
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
Err(anyhow!("Failed to establish hvsock connection with agent"))
|
||||||
// The Unix stream is now connected directly to the VSOCK socket
|
|
||||||
// the Kata agent is listening to in the VM.
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_ttrpc_client(
|
fn create_ttrpc_client(
|
||||||
@ -524,27 +549,21 @@ fn create_ttrpc_client(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let sock_addr = SockAddr::Unix(unix_addr);
|
connect(socket_fd, &unix_addr).map_err(|e| {
|
||||||
|
|
||||||
connect(socket_fd, &sock_addr).map_err(|e| {
|
|
||||||
anyhow!(e).context("Failed to connect to Unix Domain abstract socket")
|
anyhow!(e).context("Failed to connect to Unix Domain abstract socket")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
socket_fd
|
socket_fd
|
||||||
|
} else if hybrid_vsock {
|
||||||
|
let stream = setup_hybrid_vsock(&path, hybrid_vsock_port)?;
|
||||||
|
stream.into_raw_fd()
|
||||||
} else {
|
} else {
|
||||||
let stream = match UnixStream::connect(path) {
|
let stream = match UnixStream::connect(path) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(err) => {
|
||||||
return Err(
|
return Err(anyhow!("failed to setup unix stream: {:?}", err));
|
||||||
anyhow!(e).context("failed to create named UNIX Domain stream socket")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if hybrid_vsock {
|
|
||||||
setup_hybrid_vsock(&stream, hybrid_vsock_port)?
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.into_raw_fd()
|
stream.into_raw_fd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -605,7 +624,7 @@ fn announce(cfg: &Config) {
|
|||||||
info!(sl!(), "announce"; "config" => format!("{:?}", cfg));
|
info!(sl!(), "announce"; "config" => format!("{:?}", cfg));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client(cfg: &Config, commands: Vec<&str>) -> Result<()> {
|
pub fn client(cfg: &mut Config, commands: Vec<&str>) -> Result<()> {
|
||||||
if commands.len() == 1 && commands[0].eq("list") {
|
if commands.len() == 1 && commands[0].eq("list") {
|
||||||
println!("Built-in commands:\n");
|
println!("Built-in commands:\n");
|
||||||
|
|
||||||
@ -628,6 +647,68 @@ pub fn client(cfg: &Config, commands: Vec<&str>) -> Result<()> {
|
|||||||
|
|
||||||
announce(cfg);
|
announce(cfg);
|
||||||
|
|
||||||
|
let vm_ref = handle_vm(cfg)?;
|
||||||
|
|
||||||
|
info!(sl!(), "run commands");
|
||||||
|
let result = run_commands(cfg, commands);
|
||||||
|
|
||||||
|
// stop the vm if booted
|
||||||
|
if vm_ref.is_some() {
|
||||||
|
info!(sl!(), "stopping test vm");
|
||||||
|
// TODO: The error handling here is for cloud-hypervisor.
|
||||||
|
// We use tokio::runtime to call the async operations of
|
||||||
|
// runtime-rs::crates::hypervisor::_vm
|
||||||
|
// These methods can spawn some additional functions, ex
|
||||||
|
// `clh::inner_hypervisor::cloud_hypervisor_log_output`
|
||||||
|
// But since we return from the tokio::runtime block
|
||||||
|
// the runtime is dropped. During stop_vm call, cloud hypervisor
|
||||||
|
// waits for the logger task which is in cancelled state as a result.
|
||||||
|
match vm::remove_vm(vm_ref.unwrap()) {
|
||||||
|
Ok(_) => info!(sl!(), "Successfully shut down test vm"),
|
||||||
|
Err(e) => warn!(sl!(), "Error shutting down vm:{:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.map_err(|e| anyhow!(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_vm(cfg: &mut Config) -> Result<Option<vm::TestVm>> {
|
||||||
|
info!(sl!(), "handle vm request");
|
||||||
|
|
||||||
|
// Return if no vm requested
|
||||||
|
if cfg.hypervisor_name.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boot the test vm
|
||||||
|
let vm_instance = vm::setup_vm(&cfg.hypervisor_name)?;
|
||||||
|
info!(
|
||||||
|
sl!(),
|
||||||
|
"booted test vm with hypervisor: {:?}", vm_instance.hypervisor_name
|
||||||
|
);
|
||||||
|
|
||||||
|
// set the vsock server address for connecting with ttrpc server
|
||||||
|
if !vm_instance.socket_addr.is_empty() {
|
||||||
|
match vm_instance.hybrid_vsock {
|
||||||
|
true => {
|
||||||
|
// hybrid vsock URI expects unix prefix
|
||||||
|
let addr_fields: Vec<&str> = vm_instance.socket_addr.split("://").collect();
|
||||||
|
cfg.server_address = format!("{}://{}", "unix", addr_fields[1]);
|
||||||
|
cfg.hybrid_vsock = true;
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
let addr = vm_instance.socket_addr.clone();
|
||||||
|
cfg.server_address = format!("{}:{}", addr, 1024);
|
||||||
|
cfg.hybrid_vsock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(sl!(), "socket server addr: {}", cfg.server_address);
|
||||||
|
Ok(Some(vm_instance))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_commands(cfg: &Config, commands: Vec<&str>) -> Result<()> {
|
||||||
// Create separate connections for each of the services provided
|
// Create separate connections for each of the services provided
|
||||||
// by the agent.
|
// by the agent.
|
||||||
let client = kata_service_agent(
|
let client = kata_service_agent(
|
||||||
|
@ -25,6 +25,7 @@ mod image;
|
|||||||
mod rpc;
|
mod rpc;
|
||||||
mod types;
|
mod types;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
mod vm;
|
||||||
|
|
||||||
const DEFAULT_LOG_LEVEL: slog::Level = slog::Level::Info;
|
const DEFAULT_LOG_LEVEL: slog::Level = slog::Level::Info;
|
||||||
|
|
||||||
@ -73,6 +74,10 @@ fn make_examples_text(program_name: &str) -> String {
|
|||||||
# Abstract socket
|
# Abstract socket
|
||||||
$ {program} connect --server-address "{abstract_server_address}" --cmd Check
|
$ {program} connect --server-address "{abstract_server_address}" --cmd Check
|
||||||
|
|
||||||
|
- Boot up a test VM and connect to the agent (socket address determined by the tool):
|
||||||
|
|
||||||
|
$ {program} connect --vm qemu --cmd Check
|
||||||
|
|
||||||
- Query the agent environment:
|
- Query the agent environment:
|
||||||
|
|
||||||
$ {program} connect --server-address "{vsock_server_address}" --cmd GetGuestDetails
|
$ {program} connect --server-address "{vsock_server_address}" --cmd GetGuestDetails
|
||||||
@ -140,12 +145,25 @@ fn connect(name: &str, global_args: clap::ArgMatches) -> Result<()> {
|
|||||||
let interactive = args.contains_id("interactive");
|
let interactive = args.contains_id("interactive");
|
||||||
let ignore_errors = args.contains_id("ignore-errors");
|
let ignore_errors = args.contains_id("ignore-errors");
|
||||||
|
|
||||||
|
// boot-up a test vm for testing commands
|
||||||
|
let hypervisor_name = args
|
||||||
|
.get_one::<String>("vm")
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let server_address = args
|
let server_address = args
|
||||||
.get_one::<String>("server-address")
|
.get_one::<String>("server-address")
|
||||||
.map(|s| s.as_str())
|
.map(|s| s.as_str())
|
||||||
.ok_or_else(|| anyhow!("need server adddress"))?
|
.unwrap_or_default()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
// if vm is requested, we retrieve the server
|
||||||
|
// address after the boot-up is completed
|
||||||
|
if hypervisor_name.is_empty() && server_address.is_empty() {
|
||||||
|
return Err(anyhow!("need server address"));
|
||||||
|
}
|
||||||
|
|
||||||
let mut commands: Vec<&str> = Vec::new();
|
let mut commands: Vec<&str> = Vec::new();
|
||||||
|
|
||||||
if !interactive {
|
if !interactive {
|
||||||
@ -187,7 +205,7 @@ fn connect(name: &str, global_args: clap::ArgMatches) -> Result<()> {
|
|||||||
let hybrid_vsock = args.contains_id("hybrid-vsock");
|
let hybrid_vsock = args.contains_id("hybrid-vsock");
|
||||||
let no_auto_values = args.contains_id("no-auto-values");
|
let no_auto_values = args.contains_id("no-auto-values");
|
||||||
|
|
||||||
let cfg = Config {
|
let mut cfg = Config {
|
||||||
server_address,
|
server_address,
|
||||||
bundle_dir,
|
bundle_dir,
|
||||||
timeout_nano,
|
timeout_nano,
|
||||||
@ -196,9 +214,10 @@ fn connect(name: &str, global_args: clap::ArgMatches) -> Result<()> {
|
|||||||
hybrid_vsock,
|
hybrid_vsock,
|
||||||
ignore_errors,
|
ignore_errors,
|
||||||
no_auto_values,
|
no_auto_values,
|
||||||
|
hypervisor_name,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = rpc::run(&logger, &cfg, commands);
|
let result = rpc::run(&logger, &mut cfg, commands);
|
||||||
|
|
||||||
result.map_err(|e| anyhow!(e))
|
result.map_err(|e| anyhow!(e))
|
||||||
}
|
}
|
||||||
@ -283,6 +302,12 @@ fn real_main() -> Result<()> {
|
|||||||
.help("timeout value as nanoseconds or using human-readable suffixes (0 [forever], 99ns, 30us, 2ms, 5s, 7m, etc)")
|
.help("timeout value as nanoseconds or using human-readable suffixes (0 [forever], 99ns, 30us, 2ms, 5s, 7m, etc)")
|
||||||
.value_name("human-time"),
|
.value_name("human-time"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("vm")
|
||||||
|
.long("vm")
|
||||||
|
.help("boot a pod vm for testing")
|
||||||
|
.value_name("HYPERVISOR"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("generate-cid")
|
Command::new("generate-cid")
|
||||||
|
@ -11,7 +11,7 @@ use slog::{o, Logger};
|
|||||||
use crate::client::client;
|
use crate::client::client;
|
||||||
use crate::types::Config;
|
use crate::types::Config;
|
||||||
|
|
||||||
pub fn run(logger: &Logger, cfg: &Config, commands: Vec<&str>) -> Result<()> {
|
pub fn run(logger: &Logger, cfg: &mut Config, commands: Vec<&str>) -> Result<()> {
|
||||||
// Maintain the global logger for the duration of the ttRPC comms
|
// Maintain the global logger for the duration of the ttRPC comms
|
||||||
let _guard = slog_scope::set_global_logger(logger.new(o!("subsystem" => "rpc")));
|
let _guard = slog_scope::set_global_logger(logger.new(o!("subsystem" => "rpc")));
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ pub struct Config {
|
|||||||
pub hybrid_vsock: bool,
|
pub hybrid_vsock: bool,
|
||||||
pub ignore_errors: bool,
|
pub ignore_errors: bool,
|
||||||
pub no_auto_values: bool,
|
pub no_auto_values: bool,
|
||||||
|
pub hypervisor_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyFile input struct
|
// CopyFile input struct
|
||||||
|
56
src/tools/agent-ctl/src/vm/mod.rs
Normal file
56
src/tools/agent-ctl/src/vm/mod.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2024 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
//
|
||||||
|
// Description: Boot UVM for testing container storages/volumes.
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use hypervisor::Hypervisor;
|
||||||
|
use kata_types::config::{hypervisor::HYPERVISOR_NAME_CH, hypervisor::HYPERVISOR_NAME_QEMU};
|
||||||
|
use slog::info;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
mod vm_ops;
|
||||||
|
mod vm_utils;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub(crate) static ref SUPPORTED_VMMS: Vec<&'static str> =
|
||||||
|
vec![HYPERVISOR_NAME_CH, HYPERVISOR_NAME_QEMU];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TestVm {
|
||||||
|
pub hypervisor_name: String,
|
||||||
|
pub hypervisor_instance: Arc<dyn Hypervisor>,
|
||||||
|
pub socket_addr: String,
|
||||||
|
pub hybrid_vsock: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to boot a test pod VM
|
||||||
|
pub fn setup_vm(hypervisor_name: &str) -> Result<TestVm> {
|
||||||
|
info!(
|
||||||
|
sl!(),
|
||||||
|
"booting a pod vm using hypervisor:{:?}", hypervisor_name
|
||||||
|
);
|
||||||
|
|
||||||
|
if !SUPPORTED_VMMS.contains(&hypervisor_name) {
|
||||||
|
return Err(anyhow!("Unsupported hypervisor:{}", hypervisor_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?
|
||||||
|
.block_on(vm_ops::boot_vm(hypervisor_name))
|
||||||
|
.context("booting the test vm")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to stop a test pod VM
|
||||||
|
pub fn remove_vm(instance: TestVm) -> Result<()> {
|
||||||
|
info!(sl!(), "Stopping booted pod vm");
|
||||||
|
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?
|
||||||
|
.block_on(vm_ops::stop_vm(instance.hypervisor_instance))
|
||||||
|
.context("stopping the test vm")
|
||||||
|
}
|
166
src/tools/agent-ctl/src/vm/vm_ops.rs
Normal file
166
src/tools/agent-ctl/src/vm/vm_ops.rs
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
// Copyright (c) 2024 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
//
|
||||||
|
// Description: Boot UVM for testing container storages/volumes.
|
||||||
|
|
||||||
|
use crate::vm::{vm_utils, TestVm};
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use hypervisor::{
|
||||||
|
ch::CloudHypervisor,
|
||||||
|
device::{
|
||||||
|
device_manager::{do_handle_device, DeviceManager},
|
||||||
|
DeviceConfig,
|
||||||
|
},
|
||||||
|
qemu::Qemu,
|
||||||
|
BlockConfig, Hypervisor, VsockConfig,
|
||||||
|
};
|
||||||
|
use kata_types::config::{
|
||||||
|
hypervisor::register_hypervisor_plugin, hypervisor::TopologyConfigInfo,
|
||||||
|
hypervisor::HYPERVISOR_NAME_CH, hypervisor::HYPERVISOR_NAME_QEMU, CloudHypervisorConfig,
|
||||||
|
QemuConfig,
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
// Clh specific configuration path
|
||||||
|
const CLH_CONFIG_PATH: &str =
|
||||||
|
"/opt/kata/share/defaults/kata-containers/runtime-rs/configuration-cloud-hypervisor.toml";
|
||||||
|
|
||||||
|
// qemu specific configuration path
|
||||||
|
const QEMU_CONFIG_PATH: &str =
|
||||||
|
"/opt/kata/share/defaults/kata-containers/runtime-rs/configuration-qemu-runtime-rs.toml";
|
||||||
|
|
||||||
|
const VM_NAME: &str = "agent-ctl-testvm";
|
||||||
|
const VM_START_TIMEOUT: i32 = 10_000;
|
||||||
|
|
||||||
|
// Boot the test vm.
|
||||||
|
// In summary, this method
|
||||||
|
// - parses hypervisor specific kata config file
|
||||||
|
// - loads hypervisor specific config
|
||||||
|
// - instantiates a hypervisor object
|
||||||
|
// - calls prepare_vm
|
||||||
|
// - instantiates device manager to handle devices
|
||||||
|
// - calls start_vm to boot pod vm
|
||||||
|
// - retrieves the agent ttrpc server socket address
|
||||||
|
pub(crate) async fn boot_vm(name: &str) -> Result<TestVm> {
|
||||||
|
let config_path;
|
||||||
|
let mut is_hybrid_vsock = false;
|
||||||
|
|
||||||
|
// Register the hypervisor config plugin
|
||||||
|
match name {
|
||||||
|
HYPERVISOR_NAME_CH => {
|
||||||
|
register_hypervisor_plugin(HYPERVISOR_NAME_CH, Arc::new(CloudHypervisorConfig::new()));
|
||||||
|
config_path = CLH_CONFIG_PATH;
|
||||||
|
is_hybrid_vsock = true;
|
||||||
|
}
|
||||||
|
&_ => {
|
||||||
|
register_hypervisor_plugin(HYPERVISOR_NAME_QEMU, Arc::new(QemuConfig::new()));
|
||||||
|
config_path = QEMU_CONFIG_PATH;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// get the kata configuration toml
|
||||||
|
let toml_config = vm_utils::load_config(config_path)?;
|
||||||
|
|
||||||
|
let hypervisor_config = toml_config
|
||||||
|
.hypervisor
|
||||||
|
.get(name)
|
||||||
|
.ok_or_else(|| anyhow!("Failed to get hypervisor config"))
|
||||||
|
.context("get hypervisor config")?;
|
||||||
|
|
||||||
|
let hypervisor: Arc<dyn Hypervisor> = match name {
|
||||||
|
HYPERVISOR_NAME_CH => {
|
||||||
|
let hyp_ch = Arc::new(CloudHypervisor::new());
|
||||||
|
hyp_ch
|
||||||
|
.set_hypervisor_config(hypervisor_config.clone())
|
||||||
|
.await;
|
||||||
|
hyp_ch
|
||||||
|
}
|
||||||
|
&_ => {
|
||||||
|
let hyp_qemu = Arc::new(Qemu::new());
|
||||||
|
hyp_qemu
|
||||||
|
.set_hypervisor_config(hypervisor_config.clone())
|
||||||
|
.await;
|
||||||
|
hyp_qemu
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// prepare vm
|
||||||
|
// we do not pass any network namesapce since we dont want any
|
||||||
|
let empty_anno_map: HashMap<String, String> = HashMap::new();
|
||||||
|
hypervisor
|
||||||
|
.prepare_vm(VM_NAME, None, &empty_anno_map)
|
||||||
|
.await
|
||||||
|
.context(" prepare test vm")?;
|
||||||
|
|
||||||
|
// instantiate device manager
|
||||||
|
let topo_config = TopologyConfigInfo::new(&toml_config);
|
||||||
|
let dev_manager = Arc::new(RwLock::new(
|
||||||
|
DeviceManager::new(hypervisor.clone(), topo_config.as_ref())
|
||||||
|
.await
|
||||||
|
.context("failed to create device manager")?,
|
||||||
|
));
|
||||||
|
|
||||||
|
// For qemu, we need some additional device handling
|
||||||
|
// - vsock device
|
||||||
|
// - block device for rootfs if using image
|
||||||
|
if name.contains(HYPERVISOR_NAME_QEMU) {
|
||||||
|
add_vsock_device(dev_manager.clone())
|
||||||
|
.await
|
||||||
|
.context("qemu::adding vsock device")?;
|
||||||
|
if !hypervisor_config.boot_info.image.is_empty() {
|
||||||
|
let blk_config = BlockConfig {
|
||||||
|
path_on_host: hypervisor_config.boot_info.image.clone(),
|
||||||
|
is_readonly: true,
|
||||||
|
driver_option: hypervisor_config.boot_info.vm_rootfs_driver.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
add_block_device(dev_manager.clone(), blk_config)
|
||||||
|
.await
|
||||||
|
.context("qemu: handle rootfs")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start vm
|
||||||
|
hypervisor
|
||||||
|
.start_vm(VM_START_TIMEOUT)
|
||||||
|
.await
|
||||||
|
.context("start pod vm")?;
|
||||||
|
|
||||||
|
let agent_socket_addr = hypervisor
|
||||||
|
.get_agent_socket()
|
||||||
|
.await
|
||||||
|
.context("get agent socket path")?;
|
||||||
|
|
||||||
|
// return the vm structure
|
||||||
|
Ok(TestVm {
|
||||||
|
hypervisor_name: name.to_string(),
|
||||||
|
hypervisor_instance: hypervisor,
|
||||||
|
socket_addr: agent_socket_addr,
|
||||||
|
hybrid_vsock: is_hybrid_vsock,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn stop_vm(instance: Arc<dyn Hypervisor>) -> Result<()> {
|
||||||
|
instance.stop_vm().await.context("stopping pod vm")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_block_device(dev_mgr: Arc<RwLock<DeviceManager>>, cfg: BlockConfig) -> Result<()> {
|
||||||
|
do_handle_device(&dev_mgr, &DeviceConfig::BlockCfg(cfg))
|
||||||
|
.await
|
||||||
|
.context("handle block device failed")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_vsock_device(dev_mgr: Arc<RwLock<DeviceManager>>) -> Result<()> {
|
||||||
|
let vsock_config = VsockConfig {
|
||||||
|
guest_cid: libc::VMADDR_CID_ANY,
|
||||||
|
};
|
||||||
|
|
||||||
|
do_handle_device(&dev_mgr, &DeviceConfig::VsockCfg(vsock_config))
|
||||||
|
.await
|
||||||
|
.context("handle vsock device failed")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
53
src/tools/agent-ctl/src/vm/vm_utils.rs
Normal file
53
src/tools/agent-ctl/src/vm/vm_utils.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) 2025 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
//
|
||||||
|
// Description: Boot UVM for testing container storages/volumes.
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use kata_types::config::TomlConfig;
|
||||||
|
use slog::info;
|
||||||
|
|
||||||
|
// Helper function to parse a configuration file.
|
||||||
|
pub fn load_config(config_file: &str) -> Result<TomlConfig> {
|
||||||
|
info!(sl!(), "Load kata configuration file {}", config_file);
|
||||||
|
|
||||||
|
let (mut toml_config, _) = TomlConfig::load_from_file(config_file)
|
||||||
|
.context("Failed to load kata configuration file")?;
|
||||||
|
|
||||||
|
// Update the agent kernel params in hypervisor config
|
||||||
|
update_agent_kernel_params(&mut toml_config)?;
|
||||||
|
|
||||||
|
// validate configuration and return the error
|
||||||
|
toml_config.validate()?;
|
||||||
|
|
||||||
|
info!(sl!(), "parsed config content {:?}", &toml_config);
|
||||||
|
Ok(toml_config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_kernel_string(key: String, val: String) -> Result<String> {
|
||||||
|
if key.is_empty() && val.is_empty() {
|
||||||
|
Err(anyhow!("Empty key and value"))
|
||||||
|
} else if key.is_empty() {
|
||||||
|
Err(anyhow!("Empty key"))
|
||||||
|
} else if val.is_empty() {
|
||||||
|
Ok(key.to_string())
|
||||||
|
} else {
|
||||||
|
Ok(format!("{}{}{}", key, "=", val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_agent_kernel_params(config: &mut TomlConfig) -> Result<()> {
|
||||||
|
let mut params = vec![];
|
||||||
|
if let Ok(kv) = config.get_agent_kernel_params() {
|
||||||
|
for (k, v) in kv.into_iter() {
|
||||||
|
if let Ok(s) = to_kernel_string(k.to_owned(), v.to_owned()) {
|
||||||
|
params.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(h) = config.hypervisor.get_mut(&config.runtime.hypervisor_name) {
|
||||||
|
h.boot_info.add_kernel_params(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
34
tests/functional/kata-agent-apis/api-tests/test_vm_GuestDetails.bats
Executable file
34
tests/functional/kata-agent-apis/api-tests/test_vm_GuestDetails.bats
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
# Copyright (c) 2024 Microsoft Corporation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
load "${BATS_TEST_DIRNAME}/../../../common.bash"
|
||||||
|
load "${BATS_TEST_DIRNAME}/../setup_common.sh"
|
||||||
|
|
||||||
|
setup_file() {
|
||||||
|
info "setup"
|
||||||
|
sudo rm qmp.sock console.sock || echo "No existing qmp.sock/console.sock"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "Test GetGuestDetails: Boot qemu pod vm and run GetGuestDetails" {
|
||||||
|
info "Boot qemu vm, establish connection with agent inside the vm and send GetGuestDetails command"
|
||||||
|
local cmds=()
|
||||||
|
cmds+=("--vm qemu -c GetGuestDetails")
|
||||||
|
run_agent_ctl "${cmds[@]}"
|
||||||
|
sudo rm qmp.sock console.sock
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "Test GetGuestDetails: Boot cloud hypervisor pod vm and run GetGuestDetails" {
|
||||||
|
info "Boot cloud hypervisor vm, establish connection with agent inside the vm and send GetGuestDetails command"
|
||||||
|
local cmds=()
|
||||||
|
cmds+=("--vm cloud-hypervisor -c GetGuestDetails")
|
||||||
|
run_agent_ctl "${cmds[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown_file() {
|
||||||
|
info "teardown"
|
||||||
|
sudo rm -r /run/kata/agent-ctl-testvm || echo "Failed to clean /run/kata/agent-ctl-testvm"
|
||||||
|
sudo rm -r /run/kata-containers/ || echo "Failed to clean /run/kata-containers"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user