use std::path::PathBuf;
use crate::boot::BootConfig;
use crate::config::VmConfig;
use crate::disk::DiskConfig;
use crate::network::NetworkConfig;
use crate::serial::SerialConfig;
use crate::shared_dir::SharedDirConfig;
use crate::types::{MacAddress, VmId};
use crate::KasouError;
#[derive(Debug, Clone)]
enum BootMode {
Linux {
kernel: Option<PathBuf>,
initrd: Option<PathBuf>,
cmdline: Option<String>,
},
Efi {
variable_store: Option<PathBuf>,
},
}
impl Default for BootMode {
fn default() -> Self {
Self::Linux {
kernel: None,
initrd: None,
cmdline: None,
}
}
}
pub struct VmConfigBuilder {
id: VmId,
cpus: u32,
memory_mib: u64,
boot_mode: BootMode,
disks: Vec<DiskConfig>,
mac: Option<String>,
serial: Option<SerialConfig>,
shared_dirs: Vec<SharedDirConfig>,
machine_identifier_path: Option<std::path::PathBuf>,
}
impl VmConfigBuilder {
pub fn new(id: impl Into<VmId>) -> Self {
Self {
id: id.into(),
cpus: 2,
memory_mib: 2048,
boot_mode: BootMode::default(),
disks: Vec::new(),
mac: None,
serial: None,
shared_dirs: Vec::new(),
machine_identifier_path: None,
}
}
#[must_use]
pub fn machine_identifier_path(
mut self,
path: impl Into<std::path::PathBuf>,
) -> Self {
self.machine_identifier_path = Some(path.into());
self
}
pub fn cpus(mut self, count: u32) -> Self {
self.cpus = count;
self
}
pub fn memory_mib(mut self, size: u64) -> Self {
self.memory_mib = size;
self
}
pub fn boot(mut self, kernel: PathBuf) -> Self {
self.boot_mode = match self.boot_mode {
BootMode::Linux { initrd, cmdline, .. } => BootMode::Linux {
kernel: Some(kernel),
initrd,
cmdline,
},
BootMode::Efi { .. } => BootMode::Linux {
kernel: Some(kernel),
initrd: None,
cmdline: None,
},
};
self
}
pub fn initrd(mut self, initrd: PathBuf) -> Self {
self.boot_mode = match self.boot_mode {
BootMode::Linux { kernel, cmdline, .. } => BootMode::Linux {
kernel,
initrd: Some(initrd),
cmdline,
},
BootMode::Efi { .. } => BootMode::Linux {
kernel: None,
initrd: Some(initrd),
cmdline: None,
},
};
self
}
pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
self.boot_mode = match self.boot_mode {
BootMode::Linux { kernel, initrd, .. } => BootMode::Linux {
kernel,
initrd,
cmdline: Some(cmdline.into()),
},
BootMode::Efi { .. } => BootMode::Linux {
kernel: None,
initrd: None,
cmdline: Some(cmdline.into()),
},
};
self
}
pub fn efi_boot(mut self) -> Self {
self.boot_mode = match self.boot_mode {
BootMode::Efi { variable_store } => BootMode::Efi { variable_store },
BootMode::Linux { .. } => BootMode::Efi { variable_store: None },
};
self
}
pub fn efi_variable_store(mut self, path: PathBuf) -> Self {
self.boot_mode = BootMode::Efi {
variable_store: Some(path),
};
self
}
pub fn disk(mut self, path: PathBuf) -> Self {
self.disks.push(DiskConfig {
path,
read_only: false,
});
self
}
pub fn disk_readonly(mut self, path: PathBuf) -> Self {
self.disks.push(DiskConfig {
path,
read_only: true,
});
self
}
pub fn nat_network(self) -> Self {
self
}
pub fn mac(mut self, mac: impl Into<String>) -> Self {
self.mac = Some(mac.into());
self
}
pub fn deterministic_mac(mut self, seed: &str) -> Self {
self.mac = Some(MacAddress::deterministic(seed, &self.id.0).to_string());
self
}
pub fn serial_file(mut self, path: PathBuf) -> Self {
self.serial = Some(SerialConfig { log_path: path });
self
}
pub fn shared_dir(
mut self,
tag: impl Into<String>,
host_path: PathBuf,
read_only: bool,
) -> Self {
self.shared_dirs.push(SharedDirConfig {
tag: tag.into(),
host_path,
read_only,
});
self
}
pub fn build(self) -> Result<VmConfig, KasouError> {
let boot = match self.boot_mode {
BootMode::Linux { kernel, initrd, cmdline } => {
let kernel = kernel.ok_or_else(|| {
KasouError::Validation("kernel path is required (call .boot())".into())
})?;
let initrd = initrd.ok_or_else(|| {
KasouError::Validation("initrd path is required (call .initrd())".into())
})?;
BootConfig::Linux {
kernel,
initrd,
cmdline: cmdline.unwrap_or_default(),
}
}
BootMode::Efi { variable_store } => BootConfig::Efi { variable_store },
};
let config = VmConfig {
id: self.id,
cpus: self.cpus,
memory_mib: self.memory_mib,
boot,
disks: self.disks,
network: NetworkConfig {
mac_address: self.mac,
},
serial: self.serial,
shared_dirs: self.shared_dirs,
machine_identifier_path: self.machine_identifier_path,
};
config.validate()?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn builder_validates_missing_kernel() {
let result = VmConfigBuilder::new("test")
.initrd(PathBuf::from("/initrd"))
.disk(PathBuf::from("/tmp/disk.img"))
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("kernel"));
}
#[test]
fn builder_validates_missing_initrd() {
let result = VmConfigBuilder::new("test")
.boot(PathBuf::from("/kernel"))
.disk(PathBuf::from("/tmp/disk.img"))
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("initrd"));
}
#[test]
fn builder_order_independent() {
let builder = VmConfigBuilder::new("test")
.cmdline("console=hvc0")
.initrd(PathBuf::from("/initrd"))
.boot(PathBuf::from("/kernel"))
.disk(PathBuf::from("/disk.img"));
match &builder.boot_mode {
BootMode::Linux { kernel, initrd, cmdline } => {
assert!(kernel.is_some());
assert!(initrd.is_some());
assert!(cmdline.is_some());
}
BootMode::Efi { .. } => panic!("expected Linux mode"),
}
}
#[test]
fn builder_efi_mode_no_kernel_required() {
let config = VmConfigBuilder::new("brasa-test")
.cpus(2)
.memory_mib(1024)
.efi_boot()
.disk_readonly(PathBuf::from("/tmp/brasa.img"))
.build();
match config {
Ok(_) => {}
Err(e) => {
let msg = e.to_string();
assert!(
!msg.contains("kernel") && !msg.contains("initrd"),
"EFI mode should not require kernel/initrd, got: {msg}"
);
}
}
}
#[test]
fn builder_efi_variable_store_implies_efi_mode() {
let builder = VmConfigBuilder::new("test")
.efi_variable_store(PathBuf::from("/tmp/efi.vars"));
match &builder.boot_mode {
BootMode::Efi { variable_store: Some(p) } => {
assert_eq!(p, &PathBuf::from("/tmp/efi.vars"));
}
_ => panic!("expected Efi mode with variable store"),
}
}
#[test]
fn builder_sets_deterministic_mac() {
let builder = VmConfigBuilder::new("cid-k3s").deterministic_mac("my-host");
assert!(builder.mac.is_some());
assert!(builder.mac.unwrap().starts_with("52:55:55:"));
}
#[test]
fn builder_fluent_api() {
let builder = VmConfigBuilder::new("test")
.cpus(4)
.memory_mib(8192)
.boot(PathBuf::from("/kernel"))
.initrd(PathBuf::from("/initrd"))
.cmdline("console=hvc0")
.disk(PathBuf::from("/root.img"))
.disk_readonly(PathBuf::from("/seed.img"))
.nat_network()
.mac("5a:94:ef:ab:cd:12")
.serial_file(PathBuf::from("/console.log"))
.shared_dir("share0", PathBuf::from("/tmp/share"), true);
assert_eq!(builder.cpus, 4);
assert_eq!(builder.memory_mib, 8192);
assert_eq!(builder.disks.len(), 2);
assert!(matches!(builder.boot_mode, BootMode::Linux { kernel: Some(_), .. }));
assert!(builder.serial.is_some());
assert_eq!(builder.shared_dirs.len(), 1);
}
}