squib_api/schemas/
machine_config.rs1use serde::{Deserialize, Serialize};
16
17use super::common::{MAX_VCPU_COUNT, MemSizeMib};
18
19#[derive(Debug, Clone, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct RawMachineConfig {
23 pub vcpu_count: u32,
25 pub mem_size_mib: u64,
27 #[serde(default)]
29 pub smt: bool,
30 #[serde(default)]
32 pub track_dirty_pages: bool,
33 #[serde(default)]
35 pub cpu_template: Option<String>,
36 #[serde(default)]
38 pub huge_pages: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize)]
43#[non_exhaustive]
44pub struct MachineConfig {
45 pub vcpu_count: u32,
47 pub mem_size_mib: MemSizeMib,
49 pub smt: bool,
51 pub track_dirty_pages: bool,
53 pub cpu_template: Option<String>,
55 pub huge_pages: Option<String>,
57}
58
59impl TryFrom<RawMachineConfig> for MachineConfig {
60 type Error = String;
61
62 fn try_from(raw: RawMachineConfig) -> Result<Self, Self::Error> {
63 if raw.vcpu_count == 0 || raw.vcpu_count > MAX_VCPU_COUNT {
64 return Err(format!(
65 "Invalid vcpu_count: must be in 1..={MAX_VCPU_COUNT}, got {}",
66 raw.vcpu_count
67 ));
68 }
69 if raw.smt {
70 return Err("Invalid arch field for SMT: SMT not supported on Apple Silicon".into());
72 }
73 let mem_size_mib = MemSizeMib::new(raw.mem_size_mib)?;
74 if let Some(t) = raw.cpu_template.as_deref() {
75 if t.is_empty() {
76 return Err("Invalid cpu_template: must not be empty".into());
77 }
78 if t.len() > 64 {
79 return Err("Invalid cpu_template: exceeds 64 bytes".into());
80 }
81 }
82 if let Some(h) = raw.huge_pages.as_deref()
83 && (h.is_empty() || h.len() > 16)
84 {
85 return Err("Invalid huge_pages: empty or exceeds 16 bytes".into());
86 }
87 Ok(Self {
88 vcpu_count: raw.vcpu_count,
89 mem_size_mib,
90 smt: raw.smt,
91 track_dirty_pages: raw.track_dirty_pages,
92 cpu_template: raw.cpu_template,
93 huge_pages: raw.huge_pages,
94 })
95 }
96}
97
98#[derive(Debug, Clone, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct RawMachineConfigPatch {
102 #[serde(default)]
104 pub vcpu_count: Option<u32>,
105 #[serde(default)]
107 pub mem_size_mib: Option<u64>,
108 #[serde(default)]
110 pub smt: Option<bool>,
111 #[serde(default)]
113 pub track_dirty_pages: Option<bool>,
114 #[serde(default)]
116 pub cpu_template: Option<String>,
117 #[serde(default)]
119 pub huge_pages: Option<String>,
120}
121
122#[derive(Debug, Clone, Serialize)]
124#[non_exhaustive]
125pub struct MachineConfigPatch {
126 pub vcpu_count: Option<u32>,
128 pub mem_size_mib: Option<MemSizeMib>,
130 pub smt: Option<bool>,
132 pub track_dirty_pages: Option<bool>,
134 pub cpu_template: Option<String>,
136 pub huge_pages: Option<String>,
138}
139
140impl TryFrom<RawMachineConfigPatch> for MachineConfigPatch {
141 type Error = String;
142
143 fn try_from(raw: RawMachineConfigPatch) -> Result<Self, Self::Error> {
144 if let Some(v) = raw.vcpu_count
145 && (v == 0 || v > MAX_VCPU_COUNT)
146 {
147 return Err(format!(
148 "Invalid vcpu_count: must be in 1..={MAX_VCPU_COUNT}, got {v}"
149 ));
150 }
151 if let Some(true) = raw.smt {
152 return Err("Invalid arch field for SMT: SMT not supported on Apple Silicon".into());
153 }
154 let mem_size_mib = match raw.mem_size_mib {
155 Some(v) => Some(MemSizeMib::new(v)?),
156 None => None,
157 };
158 Ok(Self {
159 vcpu_count: raw.vcpu_count,
160 mem_size_mib,
161 smt: raw.smt,
162 track_dirty_pages: raw.track_dirty_pages,
163 cpu_template: raw.cpu_template,
164 huge_pages: raw.huge_pages,
165 })
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 fn raw(vcpu: u32, mem: u64) -> RawMachineConfig {
174 RawMachineConfig {
175 vcpu_count: vcpu,
176 mem_size_mib: mem,
177 smt: false,
178 track_dirty_pages: false,
179 cpu_template: None,
180 huge_pages: None,
181 }
182 }
183
184 #[test]
185 fn test_should_accept_minimum_machine_config() {
186 let mc = MachineConfig::try_from(raw(1, 256)).unwrap();
187 assert_eq!(mc.vcpu_count, 1);
188 assert_eq!(mc.mem_size_mib.get(), 256);
189 }
190
191 #[test]
192 fn test_should_accept_maximum_vcpu_count() {
193 let mc = MachineConfig::try_from(raw(32, 256)).unwrap();
194 assert_eq!(mc.vcpu_count, 32);
195 }
196
197 #[test]
198 fn test_should_reject_zero_vcpu_count() {
199 assert!(MachineConfig::try_from(raw(0, 256)).is_err());
200 }
201
202 #[test]
203 fn test_should_reject_vcpu_count_above_32() {
204 let err = MachineConfig::try_from(raw(33, 256)).unwrap_err();
205 assert!(err.contains("vcpu_count"));
206 }
207
208 #[test]
209 fn test_should_reject_smt_true_with_upstream_message() {
210 let mut r = raw(1, 256);
211 r.smt = true;
212 let err = MachineConfig::try_from(r).unwrap_err();
213 assert!(err.contains("SMT not supported on Apple Silicon"));
214 }
215
216 #[test]
217 fn test_should_reject_zero_mem_size_mib() {
218 assert!(MachineConfig::try_from(raw(1, 0)).is_err());
219 }
220
221 #[test]
222 fn test_should_reject_unknown_fields() {
223 let json = r#"{"vcpu_count":1,"mem_size_mib":256,"unexpected":true}"#;
224 let res: Result<RawMachineConfig, _> = serde_json::from_str(json);
225 assert!(res.is_err());
226 }
227
228 #[test]
229 fn test_should_validate_patch_partial_fields() {
230 let raw = RawMachineConfigPatch {
231 vcpu_count: Some(4),
232 mem_size_mib: None,
233 smt: None,
234 track_dirty_pages: Some(true),
235 cpu_template: None,
236 huge_pages: None,
237 };
238 let p = MachineConfigPatch::try_from(raw).unwrap();
239 assert_eq!(p.vcpu_count, Some(4));
240 assert_eq!(p.track_dirty_pages, Some(true));
241 }
242}