use serde::{Deserialize, Serialize};
use super::common::{MAX_VCPU_COUNT, MemSizeMib};
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawMachineConfig {
pub vcpu_count: u32,
pub mem_size_mib: u64,
#[serde(default)]
pub smt: bool,
#[serde(default)]
pub track_dirty_pages: bool,
#[serde(default)]
pub cpu_template: Option<String>,
#[serde(default)]
pub huge_pages: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct MachineConfig {
pub vcpu_count: u32,
pub mem_size_mib: MemSizeMib,
pub smt: bool,
pub track_dirty_pages: bool,
pub cpu_template: Option<String>,
pub huge_pages: Option<String>,
}
impl TryFrom<RawMachineConfig> for MachineConfig {
type Error = String;
fn try_from(raw: RawMachineConfig) -> Result<Self, Self::Error> {
if raw.vcpu_count == 0 || raw.vcpu_count > MAX_VCPU_COUNT {
return Err(format!(
"Invalid vcpu_count: must be in 1..={MAX_VCPU_COUNT}, got {}",
raw.vcpu_count
));
}
if raw.smt {
return Err("Invalid arch field for SMT: SMT not supported on Apple Silicon".into());
}
let mem_size_mib = MemSizeMib::new(raw.mem_size_mib)?;
if let Some(t) = raw.cpu_template.as_deref() {
if t.is_empty() {
return Err("Invalid cpu_template: must not be empty".into());
}
if t.len() > 64 {
return Err("Invalid cpu_template: exceeds 64 bytes".into());
}
}
if let Some(h) = raw.huge_pages.as_deref()
&& (h.is_empty() || h.len() > 16)
{
return Err("Invalid huge_pages: empty or exceeds 16 bytes".into());
}
Ok(Self {
vcpu_count: raw.vcpu_count,
mem_size_mib,
smt: raw.smt,
track_dirty_pages: raw.track_dirty_pages,
cpu_template: raw.cpu_template,
huge_pages: raw.huge_pages,
})
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawMachineConfigPatch {
#[serde(default)]
pub vcpu_count: Option<u32>,
#[serde(default)]
pub mem_size_mib: Option<u64>,
#[serde(default)]
pub smt: Option<bool>,
#[serde(default)]
pub track_dirty_pages: Option<bool>,
#[serde(default)]
pub cpu_template: Option<String>,
#[serde(default)]
pub huge_pages: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct MachineConfigPatch {
pub vcpu_count: Option<u32>,
pub mem_size_mib: Option<MemSizeMib>,
pub smt: Option<bool>,
pub track_dirty_pages: Option<bool>,
pub cpu_template: Option<String>,
pub huge_pages: Option<String>,
}
impl TryFrom<RawMachineConfigPatch> for MachineConfigPatch {
type Error = String;
fn try_from(raw: RawMachineConfigPatch) -> Result<Self, Self::Error> {
if let Some(v) = raw.vcpu_count
&& (v == 0 || v > MAX_VCPU_COUNT)
{
return Err(format!(
"Invalid vcpu_count: must be in 1..={MAX_VCPU_COUNT}, got {v}"
));
}
if let Some(true) = raw.smt {
return Err("Invalid arch field for SMT: SMT not supported on Apple Silicon".into());
}
let mem_size_mib = match raw.mem_size_mib {
Some(v) => Some(MemSizeMib::new(v)?),
None => None,
};
Ok(Self {
vcpu_count: raw.vcpu_count,
mem_size_mib,
smt: raw.smt,
track_dirty_pages: raw.track_dirty_pages,
cpu_template: raw.cpu_template,
huge_pages: raw.huge_pages,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn raw(vcpu: u32, mem: u64) -> RawMachineConfig {
RawMachineConfig {
vcpu_count: vcpu,
mem_size_mib: mem,
smt: false,
track_dirty_pages: false,
cpu_template: None,
huge_pages: None,
}
}
#[test]
fn test_should_accept_minimum_machine_config() {
let mc = MachineConfig::try_from(raw(1, 256)).unwrap();
assert_eq!(mc.vcpu_count, 1);
assert_eq!(mc.mem_size_mib.get(), 256);
}
#[test]
fn test_should_accept_maximum_vcpu_count() {
let mc = MachineConfig::try_from(raw(32, 256)).unwrap();
assert_eq!(mc.vcpu_count, 32);
}
#[test]
fn test_should_reject_zero_vcpu_count() {
assert!(MachineConfig::try_from(raw(0, 256)).is_err());
}
#[test]
fn test_should_reject_vcpu_count_above_32() {
let err = MachineConfig::try_from(raw(33, 256)).unwrap_err();
assert!(err.contains("vcpu_count"));
}
#[test]
fn test_should_reject_smt_true_with_upstream_message() {
let mut r = raw(1, 256);
r.smt = true;
let err = MachineConfig::try_from(r).unwrap_err();
assert!(err.contains("SMT not supported on Apple Silicon"));
}
#[test]
fn test_should_reject_zero_mem_size_mib() {
assert!(MachineConfig::try_from(raw(1, 0)).is_err());
}
#[test]
fn test_should_reject_unknown_fields() {
let json = r#"{"vcpu_count":1,"mem_size_mib":256,"unexpected":true}"#;
let res: Result<RawMachineConfig, _> = serde_json::from_str(json);
assert!(res.is_err());
}
#[test]
fn test_should_validate_patch_partial_fields() {
let raw = RawMachineConfigPatch {
vcpu_count: Some(4),
mem_size_mib: None,
smt: None,
track_dirty_pages: Some(true),
cpu_template: None,
huge_pages: None,
};
let p = MachineConfigPatch::try_from(raw).unwrap();
assert_eq!(p.vcpu_count, Some(4));
assert_eq!(p.track_dirty_pages, Some(true));
}
}