#[cfg(unix)]
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::path::PathBuf;
#[cfg(unix)]
use std::sync::Arc;
pub const READY_MARKER: &str = "VMRS_READY";
#[cfg(unix)]
#[derive(Debug, Clone)]
pub struct VmSocketEndpoint(Arc<OwnedFd>);
#[cfg(unix)]
impl VmSocketEndpoint {
pub fn new(fd: OwnedFd) -> Self {
Self(Arc::new(fd))
}
pub fn try_clone_owned(&self) -> std::io::Result<OwnedFd> {
let duplicated = unsafe { libc::dup(self.as_raw_fd()) };
if duplicated < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(unsafe { OwnedFd::from_raw_fd(duplicated) })
}
}
#[cfg(unix)]
impl AsRawFd for VmSocketEndpoint {
fn as_raw_fd(&self) -> RawFd {
self.0.as_raw_fd()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VmmProcess {
pid: u32,
start_time_ticks: Option<u64>,
}
impl VmmProcess {
pub fn pid(&self) -> u32 {
self.pid
}
pub fn start_time_ticks(&self) -> Option<u64> {
self.start_time_ticks
}
pub fn new(pid: u32, start_time_ticks: Option<u64>) -> Self {
Self {
pid,
start_time_ticks,
}
}
}
#[derive(Debug, Clone)]
pub struct VmConfig {
pub name: String,
pub namespace: String,
pub kernel: PathBuf,
pub initramfs: Option<PathBuf>,
pub root_disk: Option<PathBuf>,
pub data_disk: Option<PathBuf>,
pub seed_iso: Option<PathBuf>,
pub cpus: usize,
pub memory_mb: usize,
pub networks: Vec<NetworkAttachment>,
pub shared_dirs: Vec<SharedDir>,
pub serial_log: PathBuf,
pub cmdline: Option<String>,
pub netns: Option<String>,
pub vsock: bool,
pub machine_id: Option<Vec<u8>>,
pub efi_variable_store: Option<PathBuf>,
pub rosetta: bool,
}
impl VmConfig {
pub fn validate(&self) -> Result<(), crate::driver::VmError> {
use crate::driver::VmError;
if self.name.is_empty() {
return Err(VmError::InvalidConfig("VM name must not be empty".into()));
}
if self.name.len() > 128 {
return Err(VmError::InvalidConfig(
"VM name must be 128 characters or fewer".into(),
));
}
if !self
.name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
{
return Err(VmError::InvalidConfig(
"VM name must contain only alphanumeric characters, hyphens, underscores, and dots"
.into(),
));
}
if self.name.starts_with('.') || self.name.starts_with('-') {
return Err(VmError::InvalidConfig(
"VM name must not start with '.' or '-'".into(),
));
}
if self.cpus == 0 {
return Err(VmError::InvalidConfig("cpus must be at least 1".into()));
}
if self.memory_mb == 0 {
return Err(VmError::InvalidConfig(
"memory_mb must be at least 1".into(),
));
}
if self.kernel.as_os_str().is_empty() {
return Err(VmError::InvalidConfig(
"kernel path must not be empty".into(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub enum NetworkAttachment {
#[cfg(unix)]
SocketPairFd(VmSocketEndpoint),
Tap { name: String, mac: Option<String> },
}
#[derive(Debug, Clone)]
pub struct SharedDir {
pub host_path: PathBuf,
pub tag: String,
pub read_only: bool,
}
#[derive(Debug, Clone)]
pub struct VmHandle {
pub name: String,
pub namespace: String,
pub state: VmState,
pub process: Option<VmmProcess>,
pub serial_log: PathBuf,
pub machine_id: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub enum VmState {
Starting,
Running,
Ready {
ip: String,
},
Paused,
Stopped,
Failed {
reason: String,
},
}
impl VmState {
pub fn is_running(&self) -> bool {
matches!(self, Self::Running | Self::Ready { .. })
}
pub fn is_ready(&self) -> bool {
matches!(self, Self::Ready { .. })
}
pub fn ip(&self) -> Option<&str> {
match self {
Self::Ready { ip } => Some(ip),
_ => None,
}
}
}
impl std::fmt::Display for VmState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VmState::Starting => write!(f, "starting"),
VmState::Running => write!(f, "running"),
VmState::Ready { ip } => write!(f, "ready ({})", ip),
VmState::Paused => write!(f, "paused"),
VmState::Stopped => write!(f, "stopped"),
VmState::Failed { reason } => write!(f, "failed: {}", reason),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vm_state_display_starting() {
assert_eq!(VmState::Starting.to_string(), "starting");
}
#[test]
fn vm_state_display_running() {
let state = VmState::Ready {
ip: "10.0.1.2".into(),
};
assert_eq!(state.to_string(), "ready (10.0.1.2)");
}
#[test]
fn vm_state_display_running_without_ready_ip() {
assert_eq!(VmState::Running.to_string(), "running");
}
#[test]
fn vm_state_display_stopped() {
assert_eq!(VmState::Stopped.to_string(), "stopped");
}
#[test]
fn vm_state_display_failed() {
let state = VmState::Failed {
reason: "timeout".into(),
};
assert_eq!(state.to_string(), "failed: timeout");
}
#[test]
fn vm_state_equality() {
assert_eq!(VmState::Starting, VmState::Starting);
assert_eq!(VmState::Stopped, VmState::Stopped);
assert_ne!(VmState::Starting, VmState::Stopped);
}
#[test]
fn vm_state_helper_methods() {
let ready = VmState::Ready {
ip: "10.0.1.2".into(),
};
assert!(VmState::Running.is_running());
assert!(!VmState::Running.is_ready());
assert!(ready.is_running());
assert!(ready.is_ready());
assert_eq!(ready.ip(), Some("10.0.1.2"));
assert_eq!(VmState::Starting.ip(), None);
}
#[test]
fn ready_marker_value() {
assert_eq!(READY_MARKER, "VMRS_READY");
}
#[test]
fn vm_state_display_paused() {
assert_eq!(VmState::Paused.to_string(), "paused");
}
fn test_vm_config(name: &str) -> VmConfig {
VmConfig {
name: name.into(),
namespace: "test".into(),
kernel: std::path::PathBuf::from("/tmp/kernel"),
initramfs: None,
root_disk: None,
data_disk: None,
seed_iso: None,
cpus: 1,
memory_mb: 256,
networks: vec![],
shared_dirs: vec![],
serial_log: std::path::PathBuf::from("/tmp/serial.log"),
cmdline: None,
netns: None,
vsock: false,
machine_id: None,
efi_variable_store: None,
rosetta: false,
}
}
#[test]
fn validate_rejects_empty_name() {
let config = test_vm_config("");
let err = config
.validate()
.expect_err("empty VM name should fail validation")
.to_string();
assert!(err.contains("empty"), "expected 'empty' in error: {}", err);
}
#[test]
fn validate_rejects_path_traversal() {
let config = test_vm_config("../etc");
let err = config
.validate()
.expect_err("path traversal characters should fail validation")
.to_string();
assert!(
err.contains("alphanumeric") || err.contains("characters"),
"expected name validation error: {}",
err
);
}
#[test]
fn validate_rejects_zero_cpus() {
let mut config = test_vm_config("good-name");
config.cpus = 0;
let err = config
.validate()
.expect_err("zero CPUs should fail validation")
.to_string();
assert!(err.contains("cpus"), "expected 'cpus' in error: {}", err);
}
#[test]
fn validate_accepts_valid_config() {
let config = test_vm_config("my-vm.01");
config
.validate()
.expect("valid config should pass validation");
}
}