diff --git a/crates/vsock/README.md b/crates/vsock/README.md index 14d084b..8e594f0 100644 --- a/crates/vsock/README.md +++ b/crates/vsock/README.md @@ -41,20 +41,38 @@ the crate are split into various files as described below: Run the vhost-user-vsock device: ``` vhost-user-vsock --guest-cid= \ - --socket= - --uds-path= + --socket= \ + --uds-path= \ [--tx-buffer-size=host packets)>] - --config= +``` +or +``` +vhost-user-vsock --vm guest_cid=,socket=,uds-path=[,tx-buffer-size=host packets)>] ``` -Configuration Example +Specify the `--vm` argument multiple times to specify multiple devices like this: +``` +vhost-user-vsock \ +--vm guest-cid=3,socket=/tmp/vhost3.socket,uds-path=/tmp/vm3.vsock \ +--vm guest-cid=4,socket=/tmp/vhost4.socket,uds-path=/tmp/vm4.vsock,tx-buffer-size=32768 +``` +Or use a configuration file: +``` +vhost-user-vsock --config= +``` + +Configuration file example: ```yaml vms: - guest_cid: 3 socket: /tmp/vhost3.socket uds_path: /tmp/vm3.sock tx_buffer_size: 65536 + - guest_cid: 4 + socket: /tmp/vhost4.socket + uds_path: /tmp/vm4.sock + tx_buffer_size: 32768 ``` Run VMM (e.g. QEMU): @@ -71,11 +89,11 @@ qemu-system-x86_64 \ ## Working example ```sh -shell1$ vhost-user-vsock --guest-cid=4 --uds-path=/tmp/vm4.vsock --socket=/tmp/vhost4.socket +shell1$ vhost-user-vsock --vm guest-cid=4,uds-path=/tmp/vm4.vsock,socket=/tmp/vhost4.socket ``` or if you want to configure the TX buffer size ```sh -shell1$ vhost-user-vsock --guest-cid=4 --uds-path=/tmp/vm4.vsock --socket=/tmp/vhost4.socket --tx-buffer-size=65536 +shell1$ vhost-user-vsock --vm guest-cid=4,uds-path=/tmp/vm4.vsock,socket=/tmp/vhost4.socket,tx-buffer-size=65536 ``` ```sh diff --git a/crates/vsock/src/main.rs b/crates/vsock/src/main.rs index 3d365d1..babb12a 100644 --- a/crates/vsock/src/main.rs +++ b/crates/vsock/src/main.rs @@ -8,32 +8,61 @@ mod vhu_vsock; mod vhu_vsock_thread; mod vsock_conn; -use std::{convert::TryFrom, sync::Arc}; +use std::{convert::TryFrom, sync::Arc, thread}; -use crate::vhu_vsock::{Error, Result, VhostUserVsockBackend, VsockConfig}; +use crate::vhu_vsock::{VhostUserVsockBackend, VsockConfig}; use clap::{Args, Parser}; use log::{info, warn}; use serde::Deserialize; +use thiserror::Error as ThisError; use vhost::{vhost_user, vhost_user::Listener}; use vhost_user_backend::VhostUserDaemon; use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; -#[derive(Args, Debug, Deserialize)] +const DEFAULT_GUEST_CID: u64 = 3; +const DEFAULT_TX_BUFFER_SIZE: u32 = 64 * 1024; + +#[derive(Debug, ThisError)] +enum CliError { + #[error("No arguments provided")] + NoArgsProvided, + #[error("Failed to parse configuration file")] + ConfigParse, +} + +#[derive(Debug, ThisError)] +enum VmArgsParseError { + #[error("Bad argument")] + BadArgument, + #[error("Invalid key `{0}`")] + InvalidKey(String), + #[error("Unable to convert string to integer: {0}")] + ParseInteger(std::num::ParseIntError), + #[error("Required key `{0}` not found")] + RequiredKeyNotFound(String), +} + +#[derive(Args, Clone, Debug, Deserialize)] struct VsockParam { /// Context identifier of the guest which uniquely identifies the device for its lifetime. - #[arg(long, default_value_t = 3, conflicts_with = "config")] + #[arg( + long, + default_value_t = DEFAULT_GUEST_CID, + conflicts_with = "config", + conflicts_with = "vm" + )] guest_cid: u64, /// Unix socket to which a hypervisor connects to and sets up the control path with the device. - #[arg(long, conflicts_with = "config")] + #[arg(long, conflicts_with = "config", conflicts_with = "vm")] socket: String, /// Unix socket to which a host-side application connects to. - #[arg(long, conflicts_with = "config")] + #[arg(long, conflicts_with = "config", conflicts_with = "vm")] uds_path: String, /// The size of the buffer used for the TX virtqueue - #[clap(long, default_value_t = 64 * 1024, conflicts_with = "config")] + #[clap(long, default_value_t = DEFAULT_TX_BUFFER_SIZE, conflicts_with = "config", conflicts_with = "vm")] tx_buffer_size: u32, } @@ -43,45 +72,100 @@ struct VsockArgs { #[command(flatten)] param: Option, + /// Device parameters corresponding to a VM in the form of comma separated key=value pairs. + /// The allowed keys are: guest_cid, socket, uds_path and tx_buffer_size + /// Example: --vm guest-cid=3,socket=/tmp/vhost3.socket,uds-path=/tmp/vm3.vsock,tx-buffer-size=65536 + /// Multiple instances of this argument can be provided to configure devices for multiple guests. + #[arg(long, conflicts_with = "config", verbatim_doc_comment, value_parser = parse_vm_params)] + vm: Option>, + /// Load from a given configuration file #[arg(long)] config: Option, } +fn parse_vm_params(s: &str) -> Result { + let mut guest_cid = None; + let mut socket = None; + let mut uds_path = None; + let mut tx_buffer_size = None; + + for arg in s.trim().split(',') { + let mut parts = arg.split('='); + let key = parts.next().ok_or(VmArgsParseError::BadArgument)?; + let val = parts.next().ok_or(VmArgsParseError::BadArgument)?; + + match key { + "guest_cid" | "guest-cid" => { + guest_cid = Some(val.parse().map_err(VmArgsParseError::ParseInteger)?) + } + "socket" => socket = Some(val.to_string()), + "uds_path" | "uds-path" => uds_path = Some(val.to_string()), + "tx_buffer_size" | "tx-buffer-size" => { + tx_buffer_size = Some(val.parse().map_err(VmArgsParseError::ParseInteger)?) + } + _ => return Err(VmArgsParseError::InvalidKey(key.to_string())), + } + } + + Ok(VsockConfig::new( + guest_cid.unwrap_or(DEFAULT_GUEST_CID), + socket.ok_or_else(|| VmArgsParseError::RequiredKeyNotFound("socket".to_string()))?, + uds_path.ok_or_else(|| VmArgsParseError::RequiredKeyNotFound("uds-path".to_string()))?, + tx_buffer_size.unwrap_or(DEFAULT_TX_BUFFER_SIZE), + )) +} + impl VsockArgs { - pub fn parse_config(&self) -> Option { + pub fn parse_config(&self) -> Option, CliError>> { if let Some(c) = &self.config { let b = config::Config::builder() .add_source(config::File::new(c.as_str(), config::FileFormat::Yaml)) .build(); if let Ok(s) = b { let mut v = s.get::>("vms").unwrap(); - if v.len() == 1 { - return v.pop().map(|vm| { - VsockConfig::new(vm.guest_cid, vm.socket, vm.uds_path, vm.tx_buffer_size) - }); + if !v.is_empty() { + let parsed: Vec = v + .drain(..) + .map(|p| { + VsockConfig::new( + p.guest_cid, + p.socket.trim().to_string(), + p.uds_path.trim().to_string(), + p.tx_buffer_size, + ) + }) + .collect(); + return Some(Ok(parsed)); + } else { + return Some(Err(CliError::ConfigParse)); } + } else { + return Some(Err(CliError::ConfigParse)); } } None } } -impl TryFrom for VsockConfig { - type Error = Error; +impl TryFrom for Vec { + type Error = CliError; - fn try_from(cmd_args: VsockArgs) -> Result { + fn try_from(cmd_args: VsockArgs) -> Result { // we try to use the configuration first, if failed, then fall back to the manual settings. match cmd_args.parse_config() { - Some(c) => Ok(c), - _ => cmd_args.param.map_or(Err(Error::ConfigParse), |p| { - Ok(Self::new( - p.guest_cid, - p.socket.trim().to_string(), - p.uds_path.trim().to_string(), - p.tx_buffer_size, - )) - }), + Some(c) => c, + _ => match cmd_args.vm { + Some(v) => Ok(v), + _ => cmd_args.param.map_or(Err(CliError::NoArgsProvided), |p| { + Ok(vec![VsockConfig::new( + p.guest_cid, + p.socket.trim().to_string(), + p.uds_path.trim().to_string(), + p.tx_buffer_size, + )]) + }), + }, } } } @@ -129,11 +213,39 @@ pub(crate) fn start_backend_server(config: VsockConfig) { } } +pub(crate) fn start_backend_servers(configs: &[VsockConfig]) { + let mut handles = Vec::new(); + + for c in configs.iter() { + let config = c.clone(); + let handle = thread::Builder::new() + .name(format!("vhu-vsock-cid-{}", c.get_guest_cid())) + .spawn(move || start_backend_server(config)) + .unwrap(); + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } +} + fn main() { env_logger::init(); - let config = VsockConfig::try_from(VsockArgs::parse()).unwrap(); - start_backend_server(config); + let mut configs = match Vec::::try_from(VsockArgs::parse()) { + Ok(c) => c, + Err(e) => { + println!("Error parsing arguments: {}", e); + return; + } + }; + + if configs.len() == 1 { + start_backend_server(configs.pop().unwrap()); + } else { + start_backend_servers(&configs); + } } #[cfg(test)] @@ -152,12 +264,14 @@ mod tests { uds_path: uds_path.to_string(), tx_buffer_size, }), + vm: None, config: None, } } fn from_file(config: &str) -> Self { VsockArgs { param: None, + vm: None, config: Some(config.to_string()), } } @@ -168,16 +282,56 @@ mod tests { fn test_vsock_config_setup() { let args = VsockArgs::from_args(3, "/tmp/vhost4.socket", "/tmp/vm4.vsock", 64 * 1024); - let config = VsockConfig::try_from(args); - assert!(config.is_ok()); + let configs = Vec::::try_from(args); + assert!(configs.is_ok()); - let config = config.unwrap(); + let configs = configs.unwrap(); + assert_eq!(configs.len(), 1); + + let config = &configs[0]; assert_eq!(config.get_guest_cid(), 3); assert_eq!(config.get_socket_path(), "/tmp/vhost4.socket"); assert_eq!(config.get_uds_path(), "/tmp/vm4.vsock"); assert_eq!(config.get_tx_buffer_size(), 64 * 1024); } + #[test] + #[serial] + fn test_vsock_config_setup_from_vm_args() { + let params = "--vm socket=/tmp/vhost3.socket,uds_path=/tmp/vm3.vsock \ + --vm socket=/tmp/vhost4.socket,uds-path=/tmp/vm4.vsock,guest-cid=4,tx_buffer_size=65536 \ + --vm guest-cid=5,socket=/tmp/vhost5.socket,uds_path=/tmp/vm5.vsock,tx-buffer-size=32768"; + + let mut params = params.split_whitespace().collect::>(); + params.insert(0, ""); // to make the test binary name agnostic + + let args = VsockArgs::parse_from(params); + + let configs = Vec::::try_from(args); + assert!(configs.is_ok()); + + let configs = configs.unwrap(); + assert_eq!(configs.len(), 3); + + let config = configs.get(0).unwrap(); + assert_eq!(config.get_guest_cid(), 3); + assert_eq!(config.get_socket_path(), "/tmp/vhost3.socket"); + assert_eq!(config.get_uds_path(), "/tmp/vm3.vsock"); + assert_eq!(config.get_tx_buffer_size(), 65536); + + let config = configs.get(1).unwrap(); + assert_eq!(config.get_guest_cid(), 4); + assert_eq!(config.get_socket_path(), "/tmp/vhost4.socket"); + assert_eq!(config.get_uds_path(), "/tmp/vm4.vsock"); + assert_eq!(config.get_tx_buffer_size(), 65536); + + let config = configs.get(2).unwrap(); + assert_eq!(config.get_guest_cid(), 5); + assert_eq!(config.get_socket_path(), "/tmp/vhost5.socket"); + assert_eq!(config.get_uds_path(), "/tmp/vm5.vsock"); + assert_eq!(config.get_tx_buffer_size(), 32768); + } + #[test] #[serial] fn test_vsock_config_setup_from_file() { @@ -191,7 +345,11 @@ mod tests { ) .unwrap(); let args = VsockArgs::from_file("./config.yaml"); - let config = VsockConfig::try_from(args).unwrap(); + + let configs = Vec::::try_from(args).unwrap(); + assert_eq!(configs.len(), 1); + + let config = &configs[0]; assert_eq!(config.get_guest_cid(), 4); assert_eq!(config.get_socket_path(), "/tmp/vhost4.socket"); assert_eq!(config.get_uds_path(), "/tmp/vm4.vsock"); diff --git a/crates/vsock/src/vhu_vsock.rs b/crates/vsock/src/vhu_vsock.rs index 8e0863e..3f52d95 100644 --- a/crates/vsock/src/vhu_vsock.rs +++ b/crates/vsock/src/vhu_vsock.rs @@ -123,8 +123,6 @@ pub(crate) enum Error { EmptyBackendRxQ, #[error("Failed to create an EventFd")] EventFdCreate(std::io::Error), - #[error("Failed to parse a configuration file")] - ConfigParse, } impl std::convert::From for std::io::Error {