vsock: Add support for multiple guests

Adds support for instantiating multiple `VhostUserVsockBackend`s parallely
to handle multiple guests. Extends the CLI interface to accept the config
for multiple VMs in addition to the yaml config file with the `--vm`
argument as follows:

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

Signed-off-by: Priyansh Rathi <techiepriyansh@gmail.com>
This commit is contained in:
Priyansh Rathi 2023-06-04 15:45:30 +05:30 committed by Viresh Kumar
parent f50e7147df
commit a4aabb15e1
3 changed files with 212 additions and 38 deletions

View File

@ -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=<CID assigned to the guest> \
--socket=<path to the Unix socket to be created to communicate with the VMM via the vhost-user protocol>
--uds-path=<path to the Unix socket to communicate with the guest via the virtio-vsock device>
--socket=<path to the Unix socket to be created to communicate with the VMM via the vhost-user protocol> \
--uds-path=<path to the Unix socket to communicate with the guest via the virtio-vsock device> \
[--tx-buffer-size=<size of the buffer used for the TX virtqueue (guest->host packets)>]
--config=<path to the local yaml configuration file>
```
or
```
vhost-user-vsock --vm guest_cid=<CID assigned to the guest>,socket=<path to the Unix socket to be created to communicate with the VMM via the vhost-user protocol>,uds-path=<path to the Unix socket to communicate with the guest via the virtio-vsock device>[,tx-buffer-size=<size of the buffer used for the TX virtqueue (guest->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=<path to the local yaml configuration file>
```
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

View File

@ -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<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 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<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;
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<VsockConfig> {
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<VsockParam>>("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<VsockConfig> = 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<VsockArgs> for VsockConfig {
type Error = Error;
impl TryFrom<VsockArgs> for Vec<VsockConfig> {
type Error = CliError;
fn try_from(cmd_args: VsockArgs) -> Result<Self> {
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) => 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::<VsockConfig>::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::<VsockConfig>::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::<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(), "/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::<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(), "/tmp/vhost4.socket");
assert_eq!(config.get_uds_path(), "/tmp/vm4.vsock");

View File

@ -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<Error> for std::io::Error {