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>
This commit is contained in:
65
internal/agent/runtime.go
Normal file
65
internal/agent/runtime.go
Normal file
@@ -0,0 +1,65 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user