use crate::cloudhv::errors::{CloudHypervisorError, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
const DEFAULT_BINARY_PATHS: &[&str] = &[
"/usr/local/bin/cloud-hypervisor",
"/usr/bin/cloud-hypervisor",
"cloud-hypervisor", ];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudHypervisorConfig {
pub binary_path: PathBuf,
pub api_socket_path: PathBuf,
pub log_file: Option<PathBuf>,
pub seccomp: bool,
pub landlock: bool,
}
impl CloudHypervisorConfig {
pub fn new(binary_path: impl Into<PathBuf>) -> Self {
Self {
binary_path: binary_path.into(),
api_socket_path: PathBuf::from("/tmp/ch-api.sock"),
log_file: None,
seccomp: true,
landlock: true,
}
}
pub fn new_with_defaults() -> Result<Self> {
let binary_path = Self::find_binary()?;
Ok(Self {
binary_path,
api_socket_path: PathBuf::from("/tmp/ch-api.sock"),
log_file: None,
seccomp: true,
landlock: true,
})
}
pub fn new_smart(binary_path: Option<impl Into<PathBuf>>) -> Result<Self> {
let path = if let Some(p) = binary_path {
let path = p.into();
if path.exists() {
path
} else {
return Err(CloudHypervisorError::Config(format!(
"Specified binary not found: {}",
path.display()
)));
}
} else {
Self::find_binary()?
};
Ok(Self {
binary_path: path,
api_socket_path: PathBuf::from("/tmp/ch-api.sock"),
log_file: None,
seccomp: true,
landlock: true,
})
}
fn find_binary() -> Result<PathBuf> {
for path_str in DEFAULT_BINARY_PATHS {
let path = PathBuf::from(path_str);
if !path.is_absolute() {
if let Ok(which_path) = which::which(path_str) {
return Ok(which_path);
}
} else if path.exists() {
return Ok(path);
}
}
Err(CloudHypervisorError::Config(
format!(
"Cloud Hypervisor binary not found. Searched in: {}. \
Please install cloud-hypervisor or specify the binary path explicitly.",
DEFAULT_BINARY_PATHS.join(", ")
)
))
}
pub fn with_api_socket(mut self, path: impl Into<PathBuf>) -> Self {
self.api_socket_path = path.into();
self
}
pub fn with_log_file(mut self, path: impl Into<PathBuf>) -> Self {
self.log_file = Some(path.into());
self
}
pub fn with_seccomp(mut self, enabled: bool) -> Self {
self.seccomp = enabled;
self
}
pub fn with_landlock(mut self, enabled: bool) -> Self {
self.landlock = enabled;
self
}
pub fn validate(&self) -> Result<()> {
if !self.binary_path.exists() {
return Err(CloudHypervisorError::Config(format!(
"Binary not found: {}",
self.binary_path.display()
)));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VMConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub payload: Option<PayloadConfig>,
pub cpus: CpuConfig,
pub memory: MemoryConfig,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub disks: Vec<DiskConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub net: Vec<NetConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rng: Option<RngConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub fs: Vec<FsConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub vhost_user_net: Vec<VhostUserNetConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub vhost_user_blk: Vec<VhostUserBlkConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub vfio: Vec<VfioConfig>,
pub console: ConsoleConfig,
pub serial: SerialConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PayloadConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub kernel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initramfs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cmdline: Option<String>,
}
impl Default for VMConfig {
fn default() -> Self {
Self {
payload: None,
cpus: CpuConfig::default(),
memory: MemoryConfig::default(),
disks: vec![],
net: vec![],
rng: None,
fs: vec![],
vhost_user_net: vec![],
vhost_user_blk: vec![],
vfio: vec![],
console: ConsoleConfig::default(),
serial: SerialConfig::default(),
}
}
}
impl VMConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_kernel(mut self, path: impl Into<PathBuf>) -> Self {
let payload = self.payload.get_or_insert_with(|| PayloadConfig {
kernel: None,
initramfs: None,
cmdline: None,
});
payload.kernel = Some(path.into().display().to_string());
self
}
pub fn with_initramfs(mut self, path: impl Into<PathBuf>) -> Self {
let payload = self.payload.get_or_insert_with(|| PayloadConfig {
kernel: None,
initramfs: None,
cmdline: None,
});
payload.initramfs = Some(path.into().display().to_string());
self
}
pub fn with_cmdline(mut self, args: Vec<String>) -> Self {
let payload = self.payload.get_or_insert_with(|| PayloadConfig {
kernel: None,
initramfs: None,
cmdline: None,
});
payload.cmdline = Some(args.join(" "));
self
}
pub fn with_cpus(mut self, cpus: u32) -> Self {
self.cpus.boot_vcpus = cpus;
self.cpus.max_vcpus = cpus;
self
}
pub fn with_memory(mut self, size_mb: u64) -> Self {
self.memory.size = size_mb * 1024 * 1024;
self
}
pub fn with_memory_shared(mut self, shared: bool) -> Self {
self.memory.shared = shared;
self
}
pub fn with_rng(mut self, src: RngSource) -> Self {
self.rng = Some(RngConfig { src });
self
}
pub fn add_virtio_blk(&mut self, path: impl Into<PathBuf>, readonly: bool) -> &mut Self {
self.disks.push(DiskConfig {
path: path.into(),
readonly,
direct: true,
vhost_user: false,
socket: None,
});
self
}
pub fn add_virtio_net(&mut self, tap: impl Into<String>) -> &mut Self {
self.net.push(NetConfig {
tap: Some(tap.into()),
mac: None,
vhost_user: false,
socket: None,
});
self
}
pub fn add_virtio_net_with_mac(&mut self, tap: impl Into<String>, mac: impl Into<String>) -> &mut Self {
self.net.push(NetConfig {
tap: Some(tap.into()),
mac: Some(mac.into()),
vhost_user: false,
socket: None,
});
self
}
pub fn add_virtio_rng(&mut self) -> &mut Self {
self.rng = Some(RngConfig { src: RngSource::Urandom });
self
}
pub fn add_virtio_fs(&mut self, tag: impl Into<String>, socket_path: impl Into<PathBuf>) -> &mut Self {
self.fs.push(FsConfig {
tag: tag.into(),
socket: socket_path.into(),
num_queues: 1,
queue_size: 1024,
cache_size: 8 * 1024 * 1024 * 1024,
});
self
}
pub fn add_vhost_user_net(&mut self, socket: impl Into<PathBuf>) -> &mut Self {
self.vhost_user_net.push(VhostUserNetConfig {
socket: socket.into(),
mac: None,
num_queues: 2,
queue_size: 256,
});
self
}
pub fn add_vhost_user_blk(&mut self, socket: impl Into<PathBuf>) -> &mut Self {
self.vhost_user_blk.push(VhostUserBlkConfig {
socket: socket.into(),
num_queues: 1,
queue_size: 128,
readonly: false,
});
self
}
pub fn add_vfio(&mut self, device: impl Into<String>) -> &mut Self {
self.vfio.push(VfioConfig {
device: device.into(),
});
self
}
pub fn build(self) -> Result<VMSpec> {
if self.payload.is_none() || self.payload.as_ref().unwrap().kernel.is_none() {
return Err(CloudHypervisorError::Config(
"Kernel path is required".to_string(),
));
}
if self.cpus.boot_vcpus == 0 {
return Err(CloudHypervisorError::Config(
"At least one CPU is required".to_string(),
));
}
if self.memory.size == 0 {
return Err(CloudHypervisorError::Config(
"Memory size must be greater than 0".to_string(),
));
}
Ok(VMSpec {
config: self,
id: uuid::Uuid::new_v4().to_string(),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VMSpec {
pub config: VMConfig,
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CpuConfig {
pub boot_vcpus: u32,
pub max_vcpus: u32,
}
impl Default for CpuConfig {
fn default() -> Self {
Self {
boot_vcpus: 1,
max_vcpus: 1,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
pub size: u64,
pub mergeable: bool,
pub hotplug_method: HotplugMethod,
pub hotplug_size: Option<u64>,
pub hotplugged_size: Option<u64>,
pub shared: bool,
pub hugepages: bool,
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
size: 512 * 1024 * 1024,
mergeable: false,
hotplug_method: HotplugMethod::Acpi,
hotplug_size: None,
hotplugged_size: None,
shared: false,
hugepages: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HotplugMethod {
Acpi,
VirtioMem,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiskConfig {
pub path: PathBuf,
pub readonly: bool,
pub direct: bool,
pub vhost_user: bool,
pub socket: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub tap: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mac: Option<String>,
#[serde(skip_serializing_if = "is_false")]
pub vhost_user: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub socket: Option<PathBuf>,
}
fn is_false(b: &bool) -> bool {
!b
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RngConfig {
pub src: RngSource,
}
impl Default for RngConfig {
fn default() -> Self {
Self {
src: RngSource::Urandom,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RngSource {
Urandom,
Random,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FsConfig {
pub tag: String,
pub socket: PathBuf,
pub num_queues: usize,
pub queue_size: u16,
pub cache_size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VhostUserNetConfig {
pub socket: PathBuf,
pub mac: Option<String>,
pub num_queues: usize,
pub queue_size: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VhostUserBlkConfig {
pub socket: PathBuf,
pub num_queues: usize,
pub queue_size: u16,
pub readonly: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VfioConfig {
pub device: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsoleConfig {
pub mode: ConsoleMode,
}
impl Default for ConsoleConfig {
fn default() -> Self {
Self {
mode: ConsoleMode::Off,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConsoleMode {
Off,
Tty,
File,
Null,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerialConfig {
pub mode: SerialMode,
}
impl Default for SerialConfig {
fn default() -> Self {
Self {
mode: SerialMode::Null,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SerialMode {
Off,
Tty,
File,
Null,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceConfig {
pub id: String,
pub device_type: DeviceType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum DeviceType {
Disk(DiskConfig),
Net(NetConfig),
Fs(FsConfig),
VhostUserNet(VhostUserNetConfig),
VhostUserBlk(VhostUserBlkConfig),
Vfio(VfioConfig),
}