fix(fleet): placement requires the backend's rootfs image, not just KVM

The first real microVM mission was placed on morpheus because it reports
{"microvm": true}, while only tank had rootfs-claude.ext4. It failed by name
rather than booting the wrong image — but whether a mission ran came down to
which capable node was listed first, which is a coin flip dressed as scheduling.
`missions.backend` was invisible to the scheduler.

The node now enumerates the images on its disk and reports them as a `rootfs`
ARRAY. `microvm::available_backends` lives beside `rootfs_for`, its inverse,
because the two must agree on what a backend name means; split apart, one drifts
and the scheduler starts promising images the booter cannot find. It only
advertises names `rootfs_for` would accept, and reports an empty array rather than
omitting the key — set_capabilities REPLACES, so a deleted image stops being
advertised instead of leaving a stale claim.

`nodes::online_for_backend` requires microvm AND that the node's list contains the
mission's backend. A node on an older daemon has no `rootfs` key and matches
nothing: unknown is not permission, the same treatment every other capability
gets. `backend_key` maps the three spellings of "the default image" to the one
name the node advertises, and is tested — a mismatch there would reject every node
for an ordinary mission with no backend set.

The launch error now names both halves of the fix, since "no capable node" was
true but unhelpful when the node was capable and merely lacked the image.

Mission gains `backend` on the domain struct; it was a column the executor read
from the phase query while the struct that placement uses could not see it.

464 tests pass, clippy clean.
This commit is contained in:
Omar Sobh
2026-08-05 17:22:12 -07:00
parent 1cd81a8b2a
commit d9f53a3f96
5 changed files with 171 additions and 15 deletions
+38
View File
@@ -161,6 +161,44 @@ fn rootfs_for(backend: Option<&str>) -> Result<PathBuf, String> {
Ok(path)
}
/// The backend names this node can actually boot, derived from the images on
/// disk.
///
/// Reported as a capability so **placement can require it**. Without this,
/// `missions.backend` is invisible to the scheduler: the first real microVM
/// mission was placed on morpheus because it reports `microvm: true`, while only
/// tank had `rootfs-claude.ext4`. It failed by name rather than booting the wrong
/// image — but whether a mission ran came down to which capable node was picked
/// first, which is a coin flip dressed as scheduling.
///
/// Deliberately in this module: it is the inverse of [`rootfs_for`], and the two
/// must agree about what a backend name means. Split apart, one of them drifts
/// and the scheduler starts promising images the booter cannot find.
pub fn available_backends() -> Vec<String> {
let root = work_root();
let mut out = Vec::new();
// `rootfs.ext4` is what `None`/`""`/`"default"` resolve to.
if root.join("rootfs.ext4").is_file() {
out.push("default".to_string());
}
if let Ok(entries) = std::fs::read_dir(&root) {
for e in entries.flatten() {
let name = e.file_name().to_string_lossy().to_string();
if let Some(rest) = name.strip_prefix("rootfs-") {
if let Some(backend) = rest.strip_suffix(".ext4") {
// Only what `rootfs_for` would accept, so the list cannot
// advertise a name the booter would reject.
if check_id(backend).is_ok() && e.path().is_file() {
out.push(backend.to_string());
}
}
}
}
}
out.sort();
out
}
/// The agent CLI a backend image is named for, and the command that proves it
/// is present.
///