| // SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause |
| |
| mod rxops; |
| mod rxqueue; |
| mod thread_backend; |
| mod txbuf; |
| mod vhu_vsock; |
| mod vhu_vsock_thread; |
| mod vsock_conn; |
| |
| use std::{ |
| collections::HashMap, |
| convert::TryFrom, |
| process::exit, |
| sync::{Arc, RwLock}, |
| thread, |
| }; |
| |
| use crate::vhu_vsock::{CidMap, VhostUserVsockBackend, VsockConfig}; |
| use clap::{Args, Parser}; |
| use log::{error, 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}; |
| |
| const DEFAULT_GUEST_CID: u64 = 3; |
| const DEFAULT_TX_BUFFER_SIZE: u32 = 64 * 1024; |
| const DEFAULT_GROUP_NAME: &str = "default"; |
| |
| #[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(Debug, ThisError)] |
| enum BackendError { |
| #[error("Could not create backend: {0}")] |
| CouldNotCreateBackend(vhu_vsock::Error), |
| #[error("Could not create daemon: {0}")] |
| CouldNotCreateDaemon(vhost_user_backend::Error), |
| } |
| |
| #[derive(Args, Clone, Debug)] |
| struct VsockParam { |
| /// Context identifier of the guest which uniquely identifies the device for its lifetime. |
| #[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", conflicts_with = "vm")] |
| socket: String, |
| |
| /// Unix socket to which a host-side application connects to. |
| #[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 = DEFAULT_TX_BUFFER_SIZE, conflicts_with = "config", conflicts_with = "vm")] |
| tx_buffer_size: u32, |
| |
| /// The list of group names to which the device belongs. |
| /// A group is a set of devices that allow sibling communication between their guests. |
| #[arg( |
| long, |
| default_value_t = String::from(DEFAULT_GROUP_NAME), |
| conflicts_with = "config", |
| conflicts_with = "vm", |
| verbatim_doc_comment |
| )] |
| groups: String, |
| } |
| |
| #[derive(Clone, Debug, Deserialize)] |
| struct ConfigFileVsockParam { |
| guest_cid: Option<u64>, |
| socket: String, |
| uds_path: String, |
| tx_buffer_size: Option<u32>, |
| groups: Option<String>, |
| } |
| |
| #[derive(Parser, Debug)] |
| #[command(version, about = None, long_about = None)] |
| struct VsockArgs { |
| #[command(flatten)] |
| param: Option<VsockParam>, |
| |
| /// Device parameters corresponding to a VM in the form of comma separated key=value pairs. |
| /// The allowed keys are: guest_cid, socket, uds_path, tx_buffer_size and group. |
| /// Example: |
| /// --vm guest-cid=3,socket=/tmp/vhost3.socket,uds-path=/tmp/vm3.vsock,tx-buffer-size=65536,groups=group1+group2 |
| /// 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<Vec<VsockConfig>>, |
| |
| /// Load from a given configuration file |
| #[arg(long)] |
| config: Option<String>, |
| } |
| |
| fn parse_vm_params(s: &str) -> Result<VsockConfig, VmArgsParseError> { |
| let mut guest_cid = None; |
| let mut socket = None; |
| let mut uds_path = None; |
| let mut tx_buffer_size = None; |
| let mut groups = 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)?) |
| } |
| "groups" => groups = Some(val.split('+').map(String::from).collect()), |
| _ => 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), |
| groups.unwrap_or(vec![DEFAULT_GROUP_NAME.to_string()]), |
| )) |
| } |
| |
| impl VsockArgs { |
| pub fn parse_config(&self) -> Option<Result<Vec<VsockConfig>, 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::<Vec<ConfigFileVsockParam>>("vms").unwrap(); |
| if !v.is_empty() { |
| let parsed: Vec<VsockConfig> = v |
| .drain(..) |
| .map(|p| { |
| VsockConfig::new( |
| p.guest_cid.unwrap_or(DEFAULT_GUEST_CID), |
| p.socket.trim().to_string(), |
| p.uds_path.trim().to_string(), |
| p.tx_buffer_size.unwrap_or(DEFAULT_TX_BUFFER_SIZE), |
| p.groups.map_or(vec![DEFAULT_GROUP_NAME.to_string()], |g| { |
| g.trim().split('+').map(String::from).collect() |
| }), |
| ) |
| }) |
| .collect(); |
| return Some(Ok(parsed)); |
| } else { |
| return Some(Err(CliError::ConfigParse)); |
| } |
| } else { |
| return Some(Err(CliError::ConfigParse)); |
| } |
| } |
| None |
| } |
| } |
| |
| impl TryFrom<VsockArgs> for Vec<VsockConfig> { |
| type Error = CliError; |
| |
| fn try_from(cmd_args: VsockArgs) -> Result<Self, CliError> { |
| // we try to use the configuration first, if failed, then fall back to the manual settings. |
| match cmd_args.parse_config() { |
| 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, |
| p.groups.trim().split('+').map(String::from).collect(), |
| )]) |
| }), |
| }, |
| } |
| } |
| } |
| |
| /// This is the public API through which an external program starts the |
| /// vhost-device-vsock backend server. |
| pub(crate) fn start_backend_server( |
| config: VsockConfig, |
| cid_map: Arc<RwLock<CidMap>>, |
| ) -> Result<(), BackendError> { |
| loop { |
| let backend = Arc::new( |
| VhostUserVsockBackend::new(config.clone(), cid_map.clone()) |
| .map_err(BackendError::CouldNotCreateBackend)?, |
| ); |
| |
| let listener = Listener::new(config.get_socket_path(), true).unwrap(); |
| |
| let mut daemon = VhostUserDaemon::new( |
| String::from("vhost-device-vsock"), |
| backend.clone(), |
| GuestMemoryAtomic::new(GuestMemoryMmap::new()), |
| ) |
| .map_err(BackendError::CouldNotCreateDaemon)?; |
| |
| let mut epoll_handlers = daemon.get_epoll_handlers(); |
| |
| for thread in backend.threads.iter() { |
| thread |
| .lock() |
| .unwrap() |
| .register_listeners(epoll_handlers.remove(0)); |
| } |
| |
| daemon.start(listener).unwrap(); |
| |
| match daemon.wait() { |
| Ok(()) => { |
| info!("Stopping cleanly"); |
| } |
| Err(vhost_user_backend::Error::HandleRequest( |
| vhost_user::Error::PartialMessage | vhost_user::Error::Disconnected, |
| )) => { |
| info!("vhost-user connection closed with partial message. If the VM is shutting down, this is expected behavior; otherwise, it might be a bug."); |
| } |
| Err(e) => { |
| warn!("Error running daemon: {:?}", e); |
| } |
| } |
| |
| // No matter the result, we need to shut down the worker thread. |
| backend.exit_event.write(1).unwrap(); |
| } |
| } |
| |
| pub(crate) fn start_backend_servers(configs: &[VsockConfig]) -> Result<(), BackendError> { |
| let cid_map: Arc<RwLock<CidMap>> = Arc::new(RwLock::new(HashMap::new())); |
| let mut handles = Vec::new(); |
| |
| for c in configs.iter() { |
| let config = c.clone(); |
| let cid_map = cid_map.clone(); |
| let handle = thread::Builder::new() |
| .name(format!("vhu-vsock-cid-{}", c.get_guest_cid())) |
| .spawn(move || start_backend_server(config, cid_map)) |
| .unwrap(); |
| handles.push(handle); |
| } |
| |
| for handle in handles { |
| handle.join().unwrap()?; |
| } |
| |
| Ok(()) |
| } |
| |
| fn main() { |
| env_logger::init(); |
| |
| let configs = match Vec::<VsockConfig>::try_from(VsockArgs::parse()) { |
| Ok(c) => c, |
| Err(e) => { |
| println!("Error parsing arguments: {}", e); |
| return; |
| } |
| }; |
| |
| if let Err(e) = start_backend_servers(&configs) { |
| error!("{e}"); |
| exit(1); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use std::fs::File; |
| use std::io::Write; |
| use tempfile::tempdir; |
| |
| impl VsockArgs { |
| fn from_args( |
| guest_cid: u64, |
| socket: &str, |
| uds_path: &str, |
| tx_buffer_size: u32, |
| groups: &str, |
| ) -> Self { |
| VsockArgs { |
| param: Some(VsockParam { |
| guest_cid, |
| socket: socket.to_string(), |
| uds_path: uds_path.to_string(), |
| tx_buffer_size, |
| groups: groups.to_string(), |
| }), |
| vm: None, |
| config: None, |
| } |
| } |
| fn from_file(config: &str) -> Self { |
| VsockArgs { |
| param: None, |
| vm: None, |
| config: Some(config.to_string()), |
| } |
| } |
| } |
| |
| #[test] |
| fn test_vsock_config_setup() { |
| let test_dir = tempdir().expect("Could not create a temp test directory."); |
| |
| let socket_path = test_dir.path().join("vhost4.socket").display().to_string(); |
| let uds_path = test_dir.path().join("vm4.vsock").display().to_string(); |
| let args = VsockArgs::from_args(3, &socket_path, &uds_path, 64 * 1024, "group1"); |
| |
| let configs = Vec::<VsockConfig>::try_from(args); |
| assert!(configs.is_ok()); |
| |
| 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(), socket_path); |
| assert_eq!(config.get_uds_path(), uds_path); |
| assert_eq!(config.get_tx_buffer_size(), 64 * 1024); |
| assert_eq!(config.get_groups(), vec!["group1".to_string()]); |
| |
| test_dir.close().unwrap(); |
| } |
| |
| #[test] |
| fn test_vsock_config_setup_from_vm_args() { |
| let test_dir = tempdir().expect("Could not create a temp test directory."); |
| |
| let socket_paths = [ |
| test_dir.path().join("vhost3.socket"), |
| test_dir.path().join("vhost4.socket"), |
| test_dir.path().join("vhost5.socket"), |
| ]; |
| let uds_paths = [ |
| test_dir.path().join("vm3.vsock"), |
| test_dir.path().join("vm4.vsock"), |
| test_dir.path().join("vm5.vsock"), |
| ]; |
| let params = format!( |
| "--vm socket={vhost3_socket},uds_path={vm3_vsock} \ |
| --vm socket={vhost4_socket},uds-path={vm4_vsock},guest-cid=4,tx_buffer_size=65536,groups=group1 \ |
| --vm groups=group2+group3,guest-cid=5,socket={vhost5_socket},uds_path={vm5_vsock},tx-buffer-size=32768", |
| vhost3_socket = socket_paths[0].display(), |
| vhost4_socket = socket_paths[1].display(), |
| vhost5_socket = socket_paths[2].display(), |
| vm3_vsock = uds_paths[0].display(), |
| vm4_vsock = uds_paths[1].display(), |
| vm5_vsock = uds_paths[2].display(), |
| ); |
| |
| let mut params = params.split_whitespace().collect::<Vec<&str>>(); |
| params.insert(0, ""); // to make the test binary name agnostic |
| |
| let args = VsockArgs::parse_from(params); |
| |
| let configs = Vec::<VsockConfig>::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(), |
| socket_paths[0].display().to_string() |
| ); |
| assert_eq!(config.get_uds_path(), uds_paths[0].display().to_string()); |
| assert_eq!(config.get_tx_buffer_size(), 65536); |
| assert_eq!(config.get_groups(), vec![DEFAULT_GROUP_NAME.to_string()]); |
| |
| let config = configs.get(1).unwrap(); |
| assert_eq!(config.get_guest_cid(), 4); |
| assert_eq!( |
| config.get_socket_path(), |
| socket_paths[1].display().to_string() |
| ); |
| assert_eq!(config.get_uds_path(), uds_paths[1].display().to_string()); |
| assert_eq!(config.get_tx_buffer_size(), 65536); |
| assert_eq!(config.get_groups(), vec!["group1".to_string()]); |
| |
| let config = configs.get(2).unwrap(); |
| assert_eq!(config.get_guest_cid(), 5); |
| assert_eq!( |
| config.get_socket_path(), |
| socket_paths[2].display().to_string() |
| ); |
| assert_eq!(config.get_uds_path(), uds_paths[2].display().to_string()); |
| assert_eq!(config.get_tx_buffer_size(), 32768); |
| assert_eq!( |
| config.get_groups(), |
| vec!["group2".to_string(), "group3".to_string()] |
| ); |
| |
| test_dir.close().unwrap(); |
| } |
| |
| #[test] |
| fn test_vsock_config_setup_from_file() { |
| let test_dir = tempdir().expect("Could not create a temp test directory."); |
| |
| let config_path = test_dir.path().join("config.yaml"); |
| let socket_path = test_dir.path().join("vhost4.socket"); |
| let uds_path = test_dir.path().join("vm4.vsock"); |
| |
| let mut yaml = File::create(&config_path).unwrap(); |
| yaml.write_all( |
| format!( |
| "vms: |
| - guest_cid: 4 |
| socket: {} |
| uds_path: {} |
| tx_buffer_size: 32768 |
| groups: group1+group2", |
| socket_path.display(), |
| uds_path.display(), |
| ) |
| .as_bytes(), |
| ) |
| .unwrap(); |
| let args = VsockArgs::from_file(&config_path.display().to_string()); |
| |
| let configs = Vec::<VsockConfig>::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(), socket_path.display().to_string()); |
| assert_eq!(config.get_uds_path(), uds_path.display().to_string()); |
| assert_eq!(config.get_tx_buffer_size(), 32768); |
| assert_eq!( |
| config.get_groups(), |
| vec!["group1".to_string(), "group2".to_string()] |
| ); |
| |
| // Now test that optional parameters are correctly set to their default values. |
| let mut yaml = File::create(&config_path).unwrap(); |
| yaml.write_all( |
| format!( |
| "vms: |
| - socket: {} |
| uds_path: {}", |
| socket_path.display(), |
| uds_path.display(), |
| ) |
| .as_bytes(), |
| ) |
| .unwrap(); |
| let args = VsockArgs::from_file(&config_path.display().to_string()); |
| |
| let configs = Vec::<VsockConfig>::try_from(args).unwrap(); |
| assert_eq!(configs.len(), 1); |
| |
| let config = &configs[0]; |
| assert_eq!(config.get_guest_cid(), DEFAULT_GUEST_CID); |
| assert_eq!(config.get_socket_path(), socket_path.display().to_string()); |
| assert_eq!(config.get_uds_path(), uds_path.display().to_string()); |
| assert_eq!(config.get_tx_buffer_size(), DEFAULT_TX_BUFFER_SIZE); |
| assert_eq!(config.get_groups(), vec![DEFAULT_GROUP_NAME.to_string()]); |
| |
| std::fs::remove_file(&config_path).unwrap(); |
| test_dir.close().unwrap(); |
| } |
| |
| #[test] |
| fn test_vsock_server() { |
| const CID: u64 = 3; |
| const CONN_TX_BUF_SIZE: u32 = 64 * 1024; |
| |
| let test_dir = tempdir().expect("Could not create a temp test directory."); |
| |
| let vhost_socket_path = test_dir |
| .path() |
| .join("test_vsock_server.socket") |
| .display() |
| .to_string(); |
| let vsock_socket_path = test_dir |
| .path() |
| .join("test_vsock_server.vsock") |
| .display() |
| .to_string(); |
| |
| let config = VsockConfig::new( |
| CID, |
| vhost_socket_path, |
| vsock_socket_path, |
| CONN_TX_BUF_SIZE, |
| vec![DEFAULT_GROUP_NAME.to_string()], |
| ); |
| |
| let cid_map: Arc<RwLock<CidMap>> = Arc::new(RwLock::new(HashMap::new())); |
| |
| let backend = Arc::new(VhostUserVsockBackend::new(config, cid_map).unwrap()); |
| |
| let daemon = VhostUserDaemon::new( |
| String::from("vhost-device-vsock"), |
| backend.clone(), |
| GuestMemoryAtomic::new(GuestMemoryMmap::new()), |
| ) |
| .unwrap(); |
| |
| let vring_workers = daemon.get_epoll_handlers(); |
| |
| // VhostUserVsockBackend support a single thread that handles the TX and RX queues |
| assert_eq!(backend.threads.len(), 1); |
| |
| assert_eq!(vring_workers.len(), backend.threads.len()); |
| |
| test_dir.close().unwrap(); |
| } |
| } |