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:
@@ -110,6 +110,9 @@ type ComponentSpec struct {
|
||||
Cmd []string `protobuf:"bytes,8,rep,name=cmd,proto3" json:"cmd,omitempty"`
|
||||
Routes []*RouteSpec `protobuf:"bytes,9,rep,name=routes,proto3" json:"routes,omitempty"`
|
||||
Env []string `protobuf:"bytes,10,rep,name=env,proto3" json:"env,omitempty"`
|
||||
Runtime string `protobuf:"bytes,11,opt,name=runtime,proto3" json:"runtime,omitempty"` // "container" (default) or "unikernel"
|
||||
MemoryMb int32 `protobuf:"varint,12,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"` // unikernel guest memory in MB (default 256)
|
||||
Vcpus int32 `protobuf:"varint,13,opt,name=vcpus,proto3" json:"vcpus,omitempty"` // unikernel guest vCPUs (default 1)
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -214,6 +217,27 @@ func (x *ComponentSpec) GetEnv() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ComponentSpec) GetRuntime() string {
|
||||
if x != nil {
|
||||
return x.Runtime
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ComponentSpec) GetMemoryMb() int32 {
|
||||
if x != nil {
|
||||
return x.MemoryMb
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ComponentSpec) GetVcpus() int32 {
|
||||
if x != nil {
|
||||
return x.Vcpus
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type SnapshotConfig struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` // "grpc", "cli", "exec: <cmd>", "full", or "" (default)
|
||||
@@ -3586,7 +3610,7 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
|
||||
"\x04port\x18\x02 \x01(\x05R\x04port\x12\x12\n" +
|
||||
"\x04mode\x18\x03 \x01(\tR\x04mode\x12\x1a\n" +
|
||||
"\bhostname\x18\x04 \x01(\tR\bhostname\x12\x16\n" +
|
||||
"\x06public\x18\x05 \x01(\bR\x06public\"\x80\x02\n" +
|
||||
"\x06public\x18\x05 \x01(\bR\x06public\"\xcd\x02\n" +
|
||||
"\rComponentSpec\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
|
||||
"\x05image\x18\x02 \x01(\tR\x05image\x12\x18\n" +
|
||||
@@ -3598,7 +3622,10 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
|
||||
"\x03cmd\x18\b \x03(\tR\x03cmd\x12)\n" +
|
||||
"\x06routes\x18\t \x03(\v2\x11.mcp.v1.RouteSpecR\x06routes\x12\x10\n" +
|
||||
"\x03env\x18\n" +
|
||||
" \x03(\tR\x03env\"D\n" +
|
||||
" \x03(\tR\x03env\x12\x18\n" +
|
||||
"\aruntime\x18\v \x01(\tR\aruntime\x12\x1b\n" +
|
||||
"\tmemory_mb\x18\f \x01(\x05R\bmemoryMb\x12\x14\n" +
|
||||
"\x05vcpus\x18\r \x01(\x05R\x05vcpus\"D\n" +
|
||||
"\x0eSnapshotConfig\x12\x16\n" +
|
||||
"\x06method\x18\x01 \x01(\tR\x06method\x12\x1a\n" +
|
||||
"\bexcludes\x18\x02 \x03(\tR\bexcludes\"\xe6\x01\n" +
|
||||
|
||||
Reference in New Issue
Block a user