1use std::sync::Arc;
11
12#[cfg(target_os = "linux")]
21struct PostureKernelVerifier {
22 strict: bool,
23 signing_keys: Vec<String>,
24 allowed_hashes: Vec<String>,
25 daemon: Option<Arc<boatramp_server::DaemonRuntime>>,
26}
27
28#[cfg(target_os = "linux")]
31impl std::fmt::Debug for PostureKernelVerifier {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct("PostureKernelVerifier")
34 .field("strict", &self.strict)
35 .field("signing_keys", &self.signing_keys.len())
36 .field("allowed_hashes", &self.allowed_hashes.len())
37 .field("has_daemon", &self.daemon.is_some())
38 .finish()
39 }
40}
41
42#[cfg(target_os = "linux")]
43impl boatramp_firecracker::KernelVerifier for PostureKernelVerifier {
44 fn verify(&self, bytes: &[u8], expected_hash: &str) -> std::result::Result<(), String> {
46 let sig = self
50 .daemon
51 .as_ref()
52 .and_then(|d| d.effective().default_kernel.clone())
53 .filter(|dk| dk.sha256 == expected_hash)
54 .and_then(|dk| dk.sig);
55 let kref = boatramp_core::daemon_config::KernelRef {
56 source: expected_hash.to_string(),
57 sha256: expected_hash.to_string(),
58 sig,
59 };
60 boatramp_core::kernel_trust::verify_kernel(
61 bytes,
62 &kref,
63 self.strict,
64 &self.signing_keys,
65 &self.allowed_hashes,
66 )
67 .map_err(|e| e.to_string())
68 }
69}
70
71#[cfg(target_os = "macos")]
76struct VzPostureKernelVerifier {
77 strict: bool,
78 signing_keys: Vec<String>,
79 allowed_hashes: Vec<String>,
80 daemon: Option<Arc<boatramp_server::DaemonRuntime>>,
81}
82
83#[cfg(target_os = "macos")]
84impl std::fmt::Debug for VzPostureKernelVerifier {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("VzPostureKernelVerifier")
87 .field("strict", &self.strict)
88 .field("signing_keys", &self.signing_keys.len())
89 .field("allowed_hashes", &self.allowed_hashes.len())
90 .field("has_daemon", &self.daemon.is_some())
91 .finish()
92 }
93}
94
95#[cfg(target_os = "macos")]
96impl boatramp_vz::KernelVerifier for VzPostureKernelVerifier {
97 fn verify(&self, bytes: &[u8], expected_hash: &str) -> std::result::Result<(), String> {
98 let sig = self
99 .daemon
100 .as_ref()
101 .and_then(|d| d.effective().default_kernel.clone())
102 .filter(|dk| dk.sha256 == expected_hash)
103 .and_then(|dk| dk.sig);
104 let kref = boatramp_core::daemon_config::KernelRef {
105 source: expected_hash.to_string(),
106 sha256: expected_hash.to_string(),
107 sig,
108 };
109 boatramp_core::kernel_trust::verify_kernel(
110 bytes,
111 &kref,
112 self.strict,
113 &self.signing_keys,
114 &self.allowed_hashes,
115 )
116 .map_err(|e| e.to_string())
117 }
118}
119
120#[cfg(target_os = "macos")]
127fn macos_supports_vz() -> bool {
128 if cfg!(not(target_arch = "aarch64")) {
130 return false;
131 }
132 let major = sysctl_string("kern.osproductversion")
133 .and_then(|v| v.split('.').next().and_then(|m| m.parse::<u32>().ok()));
134 matches!(major, Some(m) if m >= 15)
135}
136
137#[cfg(target_os = "macos")]
140fn sysctl_string(name: &str) -> Option<String> {
141 let out = std::process::Command::new("sysctl")
142 .args(["-n", name])
143 .output()
144 .ok()?;
145 if !out.status.success() {
146 return None;
147 }
148 Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
149}
150
151pub async fn build_compute(
158 cfg: Option<&crate::config::ComputeConfig>,
159 storage: std::sync::Arc<dyn boatramp_core::Storage>,
160 data_dir: &std::path::Path,
161 node_id: u64,
162 strict: bool,
163 daemon: Option<Arc<boatramp_server::DaemonRuntime>>,
164 worker_exe: Option<&std::path::Path>,
169) -> (
170 boatramp_core::compute::BackendRegistry,
171 boatramp_core::compute::Node,
172) {
173 use boatramp_core::compute::{BackendKind, BackendRegistry, Node};
174 let mut backends: BackendRegistry = std::collections::BTreeMap::new();
175 let empty_node = |id| Node {
176 id,
177 region: None,
178 labels: std::collections::BTreeMap::new(),
179 free_vcpus: 0,
180 free_mem_mib: 0,
181 backends: Vec::new(),
182 };
183 let Some(cfg) = cfg else {
184 return (backends, empty_node(node_id));
185 };
186
187 match boatramp_docker::DockerBackend::connect() {
189 Ok(docker) => {
190 let docker = docker
194 .with_endpoint(cfg.docker_endpoint)
195 .with_volume_mode(cfg.docker_volume_mode)
196 .with_data_dir(data_dir)
197 .with_writable_root_allowed(!strict)
198 .with_cap_add_allowed(!strict);
199 if docker.reachable().await {
200 backends.insert("docker".to_string(), std::sync::Arc::new(docker));
201 } else {
202 tracing::debug!("no reachable docker daemon; skipping docker backend");
203 }
204 }
205 Err(e) => tracing::debug!(%e, "docker backend unavailable"),
206 }
207
208 #[cfg(target_os = "linux")]
210 match worker_exe.map_or_else(std::env::current_exe, |p| Ok(p.to_path_buf())) {
211 Ok(self_exe) => match boatramp_container::ContainerBackend::new(
212 storage.clone(),
213 data_dir.to_path_buf(),
214 cfg.bridge.clone(),
215 &cfg.subnet,
216 self_exe,
217 ) {
218 Ok(c) => {
219 let c = c.with_cap_add_allowed(!strict);
222 backends.insert("container".to_string(), std::sync::Arc::new(c));
223 }
224 Err(e) => tracing::warn!(%e, "container backend unavailable"),
225 },
226 Err(e) => tracing::warn!(%e, "current_exe for container backend"),
227 }
228 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
234 if std::path::Path::new("/dev/kvm").exists() {
235 match (
236 worker_exe.map_or_else(std::env::current_exe, |p| Ok(p.to_path_buf())),
237 boatramp_core::ipam::IpPool::new(&cfg.subnet),
238 ) {
239 (Ok(self_exe), Ok(pool)) => {
240 let gateway = pool.gateway().to_string();
241 let verifier: Arc<dyn boatramp_firecracker::KernelVerifier> =
243 Arc::new(PostureKernelVerifier {
244 strict,
245 signing_keys: cfg.kernel_signing_pubkeys.clone(),
246 allowed_hashes: cfg.kernel_allowed_hashes.clone(),
247 daemon: daemon.clone(),
248 });
249 match boatramp_firecracker::EmbeddedVmmBackend::new(
250 storage.clone(),
251 self_exe, data_dir.to_path_buf(),
253 cfg.bridge.clone(),
254 gateway,
255 &cfg.subnet,
256 verifier,
257 ) {
258 Ok(vmm) => {
259 backends.insert("vmm-embedded".to_string(), std::sync::Arc::new(vmm));
260 }
261 Err(e) => tracing::warn!(%e, "embedded VMM backend unavailable"),
262 }
263 }
264 (Err(e), _) => tracing::warn!(%e, "current_exe for VMM backend"),
265 (_, Err(e)) => tracing::warn!(%e, "bad compute subnet for VMM backend"),
266 }
267 } else {
268 tracing::debug!("no /dev/kvm; skipping embedded VMM backend");
269 }
270
271 #[cfg(target_os = "macos")]
277 if macos_supports_vz() {
278 match (
279 worker_exe.map_or_else(std::env::current_exe, |p| Ok(p.to_path_buf())),
280 boatramp_core::ipam::IpPool::new(&cfg.subnet),
281 ) {
282 (Ok(self_exe), Ok(_pool)) => {
283 let verifier: Arc<dyn boatramp_vz::KernelVerifier> =
284 Arc::new(VzPostureKernelVerifier {
285 strict,
286 signing_keys: cfg.kernel_signing_pubkeys.clone(),
287 allowed_hashes: cfg.kernel_allowed_hashes.clone(),
288 daemon: daemon.clone(),
289 });
290 match boatramp_vz::VzBackend::new(
291 storage.clone(),
292 self_exe, data_dir.to_path_buf(),
294 &cfg.subnet, verifier,
296 ) {
297 Ok(vz) => {
299 let vz = vz.with_writable_root_allowed(!strict);
300 backends.insert("vmm-vz".to_string(), std::sync::Arc::new(vz));
301 }
302 Err(e) => tracing::warn!(%e, "macOS VMM backend unavailable"),
303 }
304 }
305 (Err(e), _) => tracing::warn!(%e, "current_exe for macOS VMM backend"),
306 (_, Err(e)) => tracing::warn!(%e, "bad compute subnet for macOS VMM backend"),
307 }
308 } else {
309 tracing::debug!("not Apple silicon + macOS 15+; skipping macOS VMM backend");
310 }
311
312 let _ = (&storage, data_dir); #[cfg(not(any(all(target_os = "linux", target_arch = "x86_64"), target_os = "macos")))]
317 let _ = (strict, &daemon);
318
319 let free_vcpus = if cfg.vcpus > 0 {
320 cfg.vcpus
321 } else {
322 std::thread::available_parallelism()
323 .map(|n| n.get() as u32)
324 .unwrap_or(1)
325 };
326 let free_mem_mib = if cfg.mem_mib > 0 { cfg.mem_mib } else { 1024 };
327 let advertised: Vec<BackendKind> = backends
328 .iter()
329 .map(|(id, b)| {
330 let caps = b.capabilities();
331 BackendKind {
332 id: id.clone(),
333 isolation: caps.isolation,
334 persistent_volumes: caps.persistent_volumes,
335 scale_to_zero: caps.scale_to_zero,
336 }
337 })
338 .collect();
339 tracing::info!(backends = ?advertised, free_vcpus, free_mem_mib, "compute node inventory");
340 let node = Node {
341 id: node_id,
342 region: cfg.region.clone(),
343 labels: std::collections::BTreeMap::new(),
344 free_vcpus,
345 free_mem_mib,
346 backends: advertised,
347 };
348 (backends, node)
349}