Files
mcp/internal/agent/runtime.go
Kyle Isom d56f224359 Add unikernel runtime: run services as Nanos VMs under QEMU/KVM
Implements the hypervisor design's Phase 1: a second runtime.Runtime
backend (QEMU) that runs each service component as a Nanos unikernel VM
instead of a podman container, selected per-component via a new
runtime = "unikernel" service-def field.

- internal/runtime/qemu.go: QEMURuntime. Pull extracts the ELF from the
  OCI image; Run does `ops build` + boots qemu-system-x86_64 with KVM,
  user-mode net port-forwards, QMP control socket and serial console log;
  Stop/Remove/Inspect/List/Logs map onto VM lifecycle + state dir.
- proto/registry/servicedef: add runtime, memory_mb, vcpus fields
  (registry migration 5).
- agent: holds both runtimes; runtimeFor() selects per component;
  listAllContainers() merges containers + VMs so drift/status see both.
  Unikernel runtime auto-enables on nodes with /dev/kvm + ops.

Validated end-to-end on straylight: a test service deploys via
`mcp deploy --direct`, boots as a Nanos unikernel, serves HTTP through
the agent port-forward, and reports running via `mcp status`/`mcp logs`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:54:49 -07:00

66 lines
1.8 KiB
Go

package agent
import (
"context"
"os"
"os/exec"
"path/filepath"
"git.wntrmute.dev/mc/mcp/internal/config"
"git.wntrmute.dev/mc/mcp/internal/runtime"
)
// unikernelSupported reports whether this node can run Nanos unikernels:
// it needs KVM (/dev/kvm) and the `ops` toolchain on PATH.
func unikernelSupported() bool {
if _, err := os.Stat("/dev/kvm"); err != nil {
return false
}
if _, err := exec.LookPath("ops"); err != nil {
return false
}
return true
}
// homeDir returns the agent's working directory (where images/ and vm/ live),
// derived from the registry database path (e.g. /srv/mcp/mcp.db -> /srv/mcp).
func homeDir(cfg *config.AgentConfig) string {
if cfg != nil && cfg.Database.Path != "" {
return filepath.Dir(cfg.Database.Path)
}
if h := os.Getenv("HOME"); h != "" {
return h
}
return "/srv/mcp"
}
// runtimeFor selects the runtime backend for a component's declared runtime.
// Unknown or empty runtimes fall back to the container runtime. If a service
// requests "unikernel" but this node lacks the unikernel runtime, it falls
// back to the container runtime (the master should not place it here).
func (a *Agent) runtimeFor(rt string) runtime.Runtime {
if rt == "unikernel" && a.Unikernel != nil {
return a.Unikernel
}
return a.Runtime
}
// listAllContainers returns the observed state across every configured
// runtime (containers + unikernel VMs) so reconciliation, status, and drift
// detection see the whole picture.
func (a *Agent) listAllContainers(ctx context.Context) ([]runtime.ContainerInfo, error) {
infos, err := a.Runtime.List(ctx)
if err != nil {
return nil, err
}
if a.Unikernel != nil {
vms, vmErr := a.Unikernel.List(ctx)
if vmErr == nil {
infos = append(infos, vms...)
} else if a.Logger != nil {
a.Logger.Warn("list unikernel VMs failed", "err", vmErr)
}
}
return infos, nil
}