feat(fleet): B1 — microvm runtime kind and KVM placement predicate
Phase B step 1, on top of the B0 spike that proved microVMs boot here.
KVM is a HARD predicate, not a preference. gw-04 — where every mission
runs today — is itself a VM without nested virtualisation and has no
/dev/kvm, so a microvm mission landing there cannot start at all. The
scheduler therefore has to be able to tell nodes apart, which means the
node has to report what it can host.
Nodes gain a `capabilities` jsonb, populated from a probe on the node
rather than from configuration: /dev/kvm either exists there or it does
not, and nothing on the server can make it appear. The probe OPENS the
device rather than stat-ing it, because it can exist while being
unopenable (wrong group, or a container without the device passed
through) — which is precisely how firecracker will fail.
`microvm` requires BOTH kvm and a firecracker binary. A node with KVM
but no binary looks capable by the obvious test and fails at launch; a
node with the binary but no KVM is gw-04.
Placement fails the launch when no capable node exists, rather than
letting a mission sit in 'running' with nowhere to run. An explicit
target_node_id is treated as a request, not a guarantee — it is honoured
only if that node actually reports the capability.
`capabilities` defaults to '{}' NOT NULL so a node that has never
reported fails every predicate: an unqueried node and an incapable node
must be indistinguishable to the scheduler, because scheduling onto a
node whose abilities are unknown is how you get a mission that cannot
start and does not say why. The report replaces rather than merges, so a
capability the node has LOST disappears instead of leaving a stale true.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4454a1cfd9
commit
0f7fa31f86
@@ -260,6 +260,39 @@ pub async fn on_launch(
|
||||
}
|
||||
}
|
||||
|
||||
// microVM placement. KVM is a hard predicate, not a preference: gw-04 —
|
||||
// where every mission runs today — is itself a VM without nested
|
||||
// virtualisation and has no /dev/kvm, so a microvm mission landing there
|
||||
// cannot start. Resolve a capable node now and fail the launch if there is
|
||||
// none, because the alternative is a mission that sits in 'running' having
|
||||
// never had anywhere to run.
|
||||
if mission.runtime_kind == "microvm" {
|
||||
let capable =
|
||||
cm_db::repo::nodes::online_with_capabilities(pool, mission.workspace_id, &["microvm"])
|
||||
.await
|
||||
.map_err(|e| format!("looking up microvm-capable nodes: {e}"))?;
|
||||
const HOW_TO_FIX: &str = "needs /dev/kvm and firecracker installed — \
|
||||
see scripts/fc-node-setup.sh";
|
||||
let chosen = match mission.target_node_id {
|
||||
// An explicit target is a request, not a guarantee. Honour it only
|
||||
// if the node actually reports the capability.
|
||||
Some(want) => *capable
|
||||
.iter()
|
||||
.find(|n| n.as_uuid() == want)
|
||||
.ok_or_else(|| format!("mission targets node {want}, which is not an online node reporting microvm capability ({HOW_TO_FIX})"))?,
|
||||
None => *capable
|
||||
.first()
|
||||
.ok_or_else(|| format!("no online node reports microvm capability ({HOW_TO_FIX})"))?,
|
||||
};
|
||||
sqlx::query("UPDATE missions SET target_node_id = $1, updated_at = now() WHERE id = $2")
|
||||
.bind(chosen.as_uuid())
|
||||
.bind(mission_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| format!("pin mission {mission_id} to node {chosen:?}: {e}"))?;
|
||||
eprintln!("mission_orchestrator: mission {mission_id} placed on microvm node {chosen:?}");
|
||||
}
|
||||
|
||||
// Herdr second-runtime: if runtime_kind='local_herdr', spawn a
|
||||
// pane on target_node running the first available local CLI.
|
||||
// Non-fatal on failure — the operator sees the error in server
|
||||
|
||||
Reference in New Issue
Block a user