diff --git a/README.md b/README.md index e201f65e..d0e5a2d5 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ supported VM/Sandbox monitors and unikernels: | Rumprun | Solo5-hvt, Solo5-spt | x86,aarch64 | Block/Devmapper | | Unikraft | QEMU, Firecracker | x86 | Initrd, 9pfs | | MirageOS | QEMU, Solo5-hvt, Solo5-spt | x86,aarch64 | Block/Devmapper | +| IncludeOS | QEMU, Solo5-hvt, Solo5-spt | x86,aarch64 | Block/Devmapper | | Mewz | QEMU | x86 | In-memory | | Linux | QEMU, Firecracker | x86 | Initrd, Block/Devmapper, 9pfs, Virtiofs | diff --git a/docs/unikernel-support.md b/docs/unikernel-support.md index 51ebfe78..a6eb634a 100644 --- a/docs/unikernel-support.md +++ b/docs/unikernel-support.md @@ -10,8 +10,10 @@ the cloud-native ecosystem. For that reason, `urunc` aims to support all the available unikernel frameworks and similar technologies. For the time being, `urunc` provides support for -[Unikraft](https://unikraft.org/) and -[Rumprun](https://github.com/cloudkernels/rumprun) unikernels. +[Unikraft](https://unikraft.org/), +[Rumprun](https://github.com/cloudkernels/rumprun) and +[IncludeOS](https://github.com/includeos/IncludeOS) + unikernels. ## Unikraft @@ -275,6 +277,64 @@ sudo nerdctl run -m 512M --rm -ti --runtime io.containerd.urunc.v2 harbor.nbfc.i > Note: As far as we understand, Mewz requires at least 512M of memory to properly boot. +## IncludeOS + +[IncludeOS](https://github.com/includeos/IncludeOS) is a unikernel framework +written in C++, designed for building fast, secure, and resource-efficient +cloud applications. [IncludeOS](https://github.com/includeos/IncludeOS) enables +developers to write C++ applications that compile directly into bootable +unikernels, eliminating the need for a traditional operating system. +[IncludeOS](https://github.com/includeos/IncludeOS) is particularly well-suited +for web services and cloud-native applications, providing a minimal attack +surface and excellent performance characteristics. + +[IncludeOS](https://github.com/includeos/IncludeOS) provides a modern C++ +standard library implementation and includes networking capabilities with TCP/IP +stack support. The framework focuses on simplicity and performance, making it +ideal for microservices and cloud functions. + +### VMMs and other sandbox monitors + +[IncludeOS](https://github.com/includeos/IncludeOS) can execute on top of +various hypervisors and monitors. It supports execution on +[Qemu](https://www.qemu.org/) and can also run on top of +[Solo5](https://github.com/Solo5/solo5), which provides portability across +different virtualization backends. [IncludeOS](https://github.com/includeos/IncludeOS) +can access the network through virtio-net in the case of +[Qemu](https://qemu.org) and using [Solo5](https://github.com/Solo5/solo5)'s +I/O interface in the case of [Solo5](https://github.com/Solo5/solo5). For +storage, [IncludeOS](https://github.com/includeos/IncludeOS) supports +block-based storage through virtio-block and +[Solo5](https://github.com/Solo5/solo5)'s I/O interface. + +### IncludeOS and `urunc` + +In the case of [IncludeOS](https://github.com/includeos/IncludeOS), `urunc` +provides support for both [Qemu](https://www.qemu.org/) and +[Solo5](https://github.com/Solo5/solo5) monitors (including +[Solo5-hvt](https://github.com/Solo5/solo5) and +[Solo5-spt](https://github.com/Solo5/solo5)). For all monitors, `urunc` allows +access to both network and block storage through the respective monitor's I/O +interface. + +For more information on packaging +[IncludeOS](https://github.com/includeos/IncludeOS) unikernels for `urunc`, +take a look at our [packaging](../package/) page. + +An example of [IncludeOS](https://github.com/includeos/IncludeOS) on top of +[Solo5-hvt](https://github.com/Solo5/solo5) with `urunc`: + +```bash +sudo nerdctl run --rm -ti --runtime io.containerd.urunc.v2 harbor.nbfc.io/nubificus/urunc/includeos-hvt:latest +``` + +An example of [IncludeOS](https://github.com/includeos/IncludeOS) on top of +[Qemu](https://qemu.org) with `urunc`: + +```bash +sudo nerdctl run --rm -ti --runtime io.containerd.urunc.v2 harbor.nbfc.io/nubificus/urunc/includeos-qemu:latest +``` + ## Linux [Linux](https://github.com/torvalds/linux) is maybe the most widely used kernel diff --git a/pkg/unikontainers/unikernels/includeos.go b/pkg/unikontainers/unikernels/includeos.go new file mode 100644 index 00000000..e4b8effc --- /dev/null +++ b/pkg/unikontainers/unikernels/includeos.go @@ -0,0 +1,171 @@ +// Copyright (c) 2023-2025, Nubificus LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unikernels + +import ( + "fmt" + "strings" + + "github.com/urunc-dev/urunc/pkg/unikontainers/types" +) + +const IncludeOSUnikernel string = "includeos" + +type IncludeOS struct { + Command string + Monitor string + Envs []string + Net IncludeOSNet + Block []IncludeOSBlock +} + +type IncludeOSNet struct { + Address string + Gateway string + Mask string +} + +type IncludeOSBlock struct { + ID string + HostPath string +} + +func (i *IncludeOS) CommandString() (string, error) { + cmdParts := []string{} + + // IncludeOS expects network config as a JSON string argument. + // We construct this manually to ensure it matches the specific schema required by the OS. + if i.Net.Address != "" { + // Default to iface 0. + // Constructing JSON: {"net":[{"iface":0,"address":"...","netmask":"...","gateway":"..."}]} + gwPart := "" + if i.Net.Gateway != "" { + gwPart = fmt.Sprintf(`,"gateway":"%s"`, i.Net.Gateway) + } + + // We assume i.Net.Mask is in dotted-decimal format (e.g 255.255.255.0) + // If urunc provides CIDR conversion logic would be needed here + jsonConfig := fmt.Sprintf( + `{"net":[{"iface":0,"address":"%s","netmask":"%s"%s}]}`, + i.Net.Address, + i.Net.Mask, + gwPart, + ) + cmdParts = append(cmdParts, jsonConfig) + } + + if len(i.Envs) > 0 { + cmdParts = append(cmdParts, i.Envs...) + } + + if i.Command != "" { + cmdParts = append(cmdParts, i.Command) + } + + return strings.Join(cmdParts, " "), nil +} + +func (i *IncludeOS) SupportsBlock() bool { + return true +} + +func (i *IncludeOS) SupportsFS(fsType string) bool { + switch fsType { + case "ext2", "ext3", "ext4": + return true + default: + return false + } +} + +func (i *IncludeOS) MonitorNetCli(ifName string, mac string) string { + switch i.Monitor { + case "hvt", "spt": + // Solo5 monitor options for networking + netOption := "--net:service=" + ifName + netOption += " --net-mac:service=" + mac + return netOption + case "qemu": + // QEMU handles networking through its own options in the hypervisor layer + return "" + default: + return "" + } +} + +func (i *IncludeOS) MonitorBlockCli() []types.MonitorBlockArgs { + if len(i.Block) == 0 { + return nil + } + + switch i.Monitor { + case "hvt", "spt": + // Solo5 monitors support block devices with specific IDs. + // Note: Solo5 typically supports a single block device. + blockArgs := make([]types.MonitorBlockArgs, 0, len(i.Block)) + for _, blk := range i.Block { + id := blk.ID + if id == "" { + id = "storage" + } + blockArgs = append(blockArgs, types.MonitorBlockArgs{ + ID: id, + Path: blk.HostPath, + }) + } + // Return only the first block device to ensure compatibility with Solo5 + if len(blockArgs) > 0 { + return blockArgs[:1] + } + return blockArgs + case "qemu": + // QEMU handles block devices through its own options + return nil + default: + return nil + } +} + +func (i *IncludeOS) MonitorCli() types.MonitorCliArgs { + // IncludeOS does not require any generic monitor-specific arguments + return types.MonitorCliArgs{} +} + +func (i *IncludeOS) Init(data types.UnikernelParams) error { + if data.Net.Mask != "" { + i.Net.Address = data.Net.IP + i.Net.Gateway = data.Net.Gateway + i.Net.Mask = data.Net.Mask + } + + i.Block = make([]IncludeOSBlock, 0, len(data.Block)) + for _, blk := range data.Block { + newBlk := IncludeOSBlock{ + ID: blk.ID, + HostPath: blk.Source, + } + i.Block = append(i.Block, newBlk) + } + + i.Command = strings.Join(data.CmdLine, " ") + i.Monitor = data.Monitor + i.Envs = data.EnvVars + + return nil +} + +func newIncludeOS() *IncludeOS { + return &IncludeOS{} +} diff --git a/pkg/unikontainers/unikernels/includeos_test.go b/pkg/unikontainers/unikernels/includeos_test.go new file mode 100644 index 00000000..3ce78ec0 --- /dev/null +++ b/pkg/unikontainers/unikernels/includeos_test.go @@ -0,0 +1,88 @@ +// Copyright (c) 2023-2025, Nubificus LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package unikernels + +import ( + "strings" + "testing" +) + +func TestIncludeOS_CommandString(t *testing.T) { + tests := []struct { + name string + unikernel *IncludeOS + expected []string + }{ + { + name: "Full Configuration", + unikernel: &IncludeOS{ + Command: "my_app_arg", + Envs: []string{"FOO=bar", "BAZ=qux"}, + Net: IncludeOSNet{ + Address: "192.168.1.5", + Mask: "255.255.255.0", + Gateway: "192.168.1.1", + }, + }, + expected: []string{ + `{"net":[{"iface":0,"address":"192.168.1.5","netmask":"255.255.255.0","gateway":"192.168.1.1"}]}`, + "FOO=bar", + "BAZ=qux", + "my_app_arg", + }, + }, + { + name: "Network Only (No Gateway)", + unikernel: &IncludeOS{ + Net: IncludeOSNet{ + Address: "10.0.0.2", + Mask: "255.255.0.0", + }, + }, + expected: []string{ + `{"net":[{"iface":0,"address":"10.0.0.2","netmask":"255.255.0.0"}]}`, + }, + }, + { + name: "No Network (Command Only)", + unikernel: &IncludeOS{ + Command: "just_running", + }, + expected: []string{ + "just_running", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.unikernel.CommandString() + if err != nil { + t.Fatalf("CommandString() error = %v", err) + } + + for _, part := range tt.expected { + if !strings.Contains(got, part) { + t.Errorf("CommandString() = %v\nMissing expected part: %v", got, part) + } + } + + if tt.name == "No Network (Command Only)" { + if strings.Contains(got, "net") { + t.Errorf("CommandString() should not contain network config, got: %v", got) + } + } + }) + } +} diff --git a/pkg/unikontainers/unikernels/unikernel.go b/pkg/unikontainers/unikernels/unikernel.go index d1f57cac..58b44915 100644 --- a/pkg/unikontainers/unikernels/unikernel.go +++ b/pkg/unikontainers/unikernels/unikernel.go @@ -39,6 +39,9 @@ func New(unikernelType string) (types.Unikernel, error) { case LinuxUnikernel: unikernel := newLinux() return unikernel, nil + case IncludeOSUnikernel: + unikernel := newIncludeOS() + return unikernel, nil default: return nil, ErrNotSupportedUnikernel }