Skip to main content

boatramp_node/
compute.rs

1//! Compute-backend assembly (moved from the binary — node-library N2).
2//!
3//! Builds this node's compute [`BackendRegistry`](boatramp_core::compute::BackendRegistry)
4//! and scheduler [`Node`](boatramp_core::compute::Node) inventory from the optional
5//! `[compute]` config, capability-detecting Docker, native-container, and
6//! embedded-VMM backends. Lives here (not in the backend-agnostic
7//! `boatramp-server`) because it depends on the concrete backend crates; the
8//! binary and future `assemble()` call [`build_compute`].
9
10use std::sync::Arc;
11
12/// The posture-scaled kernel-trust gate wired into the compute backends: it runs
13/// [`boatramp_core::kernel_trust::verify_kernel`] on the staged kernel right
14/// before boot. The always-on check is the content hash; under the strict
15/// (multi-tenant) posture it additionally requires the pinned hash to be on the
16/// static allow-list and to carry a signature — sourced from the **live fleet
17/// default kernel** — verifying against a static signing key. No daemon, or a hash
18/// that isn't the current signed default, has no signature source and so **fails
19/// closed** under strict: the kernel does not boot.
20#[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// `KernelVerifier` requires `Debug`, but `DaemonRuntime` isn't `Debug` (it holds a
29// lock + a `Notify`); summarise instead of recursing into it.
30#[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    // Fully-qualified: this module aliases `Result<T>` to its own error type.
45    fn verify(&self, bytes: &[u8], expected_hash: &str) -> std::result::Result<(), String> {
46        // The only signature we trust for this hash is the one on the current
47        // fleet default kernel (the operator-vetted kernel); any other hash has no
48        // signature source and fails the strict bar.
49        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/// Build this node's compute [`BackendRegistry`] + scheduler [`Node`] inventory
72/// from the optional `[compute]` config. Backends
73/// are **capability-detected**: a reachable Docker daemon ⇒ `docker`; Linux ⇒ the
74/// native `container` backend; Linux + `/dev/kvm` ⇒ the in-process
75/// `vmm-embedded` microVM backend (strongest isolation). Absent config ⇒ an empty
76/// registry + a node advertising nothing, so the reconcile loop stays a no-op.
77pub async fn build_compute(
78    cfg: Option<&crate::config::ComputeConfig>,
79    storage: std::sync::Arc<dyn boatramp_core::Storage>,
80    data_dir: &std::path::Path,
81    node_id: u64,
82    strict: bool,
83    daemon: Option<Arc<boatramp_server::DaemonRuntime>>,
84) -> (
85    boatramp_core::compute::BackendRegistry,
86    boatramp_core::compute::Node,
87) {
88    use boatramp_core::compute::{BackendKind, BackendRegistry, Node};
89    let mut backends: BackendRegistry = std::collections::BTreeMap::new();
90    let empty_node = |id| Node {
91        id,
92        region: None,
93        labels: std::collections::BTreeMap::new(),
94        free_vcpus: 0,
95        free_mem_mib: 0,
96        backends: Vec::new(),
97    };
98    let Some(cfg) = cfg else {
99        return (backends, empty_node(node_id));
100    };
101
102    // Remote docker: register only if a daemon actually answers.
103    match boatramp_docker::DockerBackend::connect() {
104        Ok(docker) => {
105            // `writable_root` is honored only under the single-tenant posture
106            // (`!strict`); the multi-tenant guard keeps the hardened read-only root.
107            let docker = docker
108                .with_endpoint(cfg.docker_endpoint)
109                .with_volume_mode(cfg.docker_volume_mode)
110                .with_data_dir(data_dir)
111                .with_writable_root_allowed(!strict);
112            if docker.reachable().await {
113                backends.insert("docker".to_string(), std::sync::Arc::new(docker));
114            } else {
115                tracing::debug!("no reachable docker daemon; skipping docker backend");
116            }
117        }
118        Err(e) => tracing::debug!(%e, "docker backend unavailable"),
119    }
120
121    // Native container backend (Linux only).
122    #[cfg(target_os = "linux")]
123    match std::env::current_exe() {
124        Ok(self_exe) => match boatramp_container::ContainerBackend::new(
125            storage.clone(),
126            data_dir.to_path_buf(),
127            cfg.bridge.clone(),
128            &cfg.subnet,
129            self_exe,
130        ) {
131            Ok(c) => {
132                backends.insert("container".to_string(), std::sync::Arc::new(c));
133            }
134            Err(e) => tracing::warn!(%e, "container backend unavailable"),
135        },
136        Err(e) => tracing::warn!(%e, "current_exe for container backend"),
137    }
138    // Embedded VMM backend (Linux + x86_64 + `/dev/kvm`): in-process microVMs, no
139    // external `firecracker` binary — the strongest isolation when KVM is available.
140    // Like the container backend it enslaves each tap to `cfg.bridge` (assumed set
141    // up). The embedded VMM is KVM-x86-specific, so this is x86_64-only; boatramp
142    // still serves on linux/aarch64 (with the container backend, no embedded VMM).
143    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
144    if std::path::Path::new("/dev/kvm").exists() {
145        match (
146            std::env::current_exe(),
147            boatramp_core::ipam::IpPool::new(&cfg.subnet),
148        ) {
149            (Ok(self_exe), Ok(pool)) => {
150                let gateway = pool.gateway().to_string();
151                // Verify-before-boot gate for every kernel this backend stages.
152                let verifier: Arc<dyn boatramp_firecracker::KernelVerifier> =
153                    Arc::new(PostureKernelVerifier {
154                        strict,
155                        signing_keys: cfg.kernel_signing_pubkeys.clone(),
156                        allowed_hashes: cfg.kernel_allowed_hashes.clone(),
157                        daemon: daemon.clone(),
158                    });
159                match boatramp_firecracker::EmbeddedVmmBackend::new(
160                    storage.clone(),
161                    self_exe, // re-exec'd as `__vmm-run` per VM (jailed subprocess)
162                    data_dir.to_path_buf(),
163                    cfg.bridge.clone(),
164                    gateway,
165                    &cfg.subnet,
166                    verifier,
167                ) {
168                    Ok(vmm) => {
169                        backends.insert("vmm-embedded".to_string(), std::sync::Arc::new(vmm));
170                    }
171                    Err(e) => tracing::warn!(%e, "embedded VMM backend unavailable"),
172                }
173            }
174            (Err(e), _) => tracing::warn!(%e, "current_exe for VMM backend"),
175            (_, Err(e)) => tracing::warn!(%e, "bad compute subnet for VMM backend"),
176        }
177    } else {
178        tracing::debug!("no /dev/kvm; skipping embedded VMM backend");
179    }
180
181    let _ = (&storage, data_dir); // used only on Linux (container / VMM backends)
182                                  // The kernel-trust verifier is wired only for the embedded VMM (x86_64 Linux);
183                                  // silence `strict`/`daemon` everywhere else (non-Linux and linux/aarch64).
184    #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
185    let _ = (strict, &daemon);
186
187    let free_vcpus = if cfg.vcpus > 0 {
188        cfg.vcpus
189    } else {
190        std::thread::available_parallelism()
191            .map(|n| n.get() as u32)
192            .unwrap_or(1)
193    };
194    let free_mem_mib = if cfg.mem_mib > 0 { cfg.mem_mib } else { 1024 };
195    let advertised: Vec<BackendKind> = backends
196        .iter()
197        .map(|(id, b)| {
198            let caps = b.capabilities();
199            BackendKind {
200                id: id.clone(),
201                isolation: caps.isolation,
202                persistent_volumes: caps.persistent_volumes,
203                scale_to_zero: caps.scale_to_zero,
204            }
205        })
206        .collect();
207    tracing::info!(backends = ?advertised, free_vcpus, free_mem_mib, "compute node inventory");
208    let node = Node {
209        id: node_id,
210        region: cfg.region.clone(),
211        labels: std::collections::BTreeMap::new(),
212        free_vcpus,
213        free_mem_mib,
214        backends: advertised,
215    };
216    (backends, node)
217}