From 22d34a8cb97aaf87631c6fc089fb15cd26cd2141 Mon Sep 17 00:00:00 2001 From: veju Date: Sun, 17 May 2026 00:55:58 +0530 Subject: [PATCH 1/3] fix: use annotation for MirageOS net device name Fixes: #315 Signed-off-by: viju --- pkg/unikontainers/config.go | 15 +++++++ pkg/unikontainers/config_test.go | 4 ++ pkg/unikontainers/types/types.go | 1 + pkg/unikontainers/unikernels/mirage.go | 20 +++++++--- pkg/unikontainers/unikernels/mirage_test.go | 44 +++++++++++++++++++++ pkg/unikontainers/unikontainers.go | 11 +++--- 6 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 pkg/unikontainers/unikernels/mirage_test.go diff --git a/pkg/unikontainers/config.go b/pkg/unikontainers/config.go index 7751aa275..21e239cf7 100644 --- a/pkg/unikontainers/config.go +++ b/pkg/unikontainers/config.go @@ -45,6 +45,7 @@ const ( annotBlock = "com.urunc.unikernel.block" annotBlockMntPoint = "com.urunc.unikernel.blkMntPoint" annotMountRootfs = "com.urunc.unikernel.mountRootfs" + annotNetDev = "com.urunc.unikernel.netDev" ) // A UnikernelConfig struct holds the info provided by bima image on how to execute our unikernel @@ -58,6 +59,7 @@ type UnikernelConfig struct { Block string `json:"com.urunc.unikernel.block,omitempty"` BlkMntPoint string `json:"com.urunc.unikernel.blkMntPoint,omitempty"` MountRootfs string `json:"com.urunc.unikernel.mountRootfs"` + NetDev string `json:"com.urunc.unikernel.netDev,omitempty"` } // validate checks if the mandatory configuration fields are present. @@ -127,6 +129,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { block := spec.Annotations[annotBlock] blkMntPoint := spec.Annotations[annotBlockMntPoint] MountRootfs := spec.Annotations[annotMountRootfs] + netDev := spec.Annotations[annotNetDev] uniklog.WithFields(logrus.Fields{ "unikernelType": tryDecode(unikernelType), "unikernelVersion": tryDecode(unikernelVersion), @@ -137,6 +140,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { "block": tryDecode(block), "blkMntPoint": tryDecode(blkMntPoint), "mountRootfs": tryDecode(MountRootfs), + "netDev": tryDecode(netDev), }).WithField("source", "spec").Debug("urunc annotations") return &UnikernelConfig{ @@ -149,6 +153,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { Block: block, BlkMntPoint: blkMntPoint, MountRootfs: MountRootfs, + NetDev: netDev, } } @@ -188,6 +193,7 @@ func getConfigFromJSON(jsonFilePath string) (*UnikernelConfig, error) { "block": tryDecode(conf.Block), "blkMntPoint": tryDecode(conf.BlkMntPoint), "mountRootfs": tryDecode(conf.MountRootfs), + "netDev": tryDecode(conf.NetDev), }).WithField("source", uruncJSONFilename).Debug("urunc annotations") return &conf, nil @@ -258,6 +264,12 @@ func (c *UnikernelConfig) decode() error { } c.MountRootfs = string(decoded) + decoded, err = base64.StdEncoding.DecodeString(c.NetDev) + if err != nil { + return fmt.Errorf("failed to decode netDev: %v", err) + } + c.NetDev = string(decoded) + return nil } @@ -291,6 +303,9 @@ func (c *UnikernelConfig) Map() map[string]string { if c.MountRootfs != "" { myMap[annotMountRootfs] = c.MountRootfs } + if c.NetDev != "" { + myMap[annotNetDev] = c.NetDev + } return myMap } diff --git a/pkg/unikontainers/config_test.go b/pkg/unikontainers/config_test.go index 5579122f3..71a9eb5c7 100644 --- a/pkg/unikontainers/config_test.go +++ b/pkg/unikontainers/config_test.go @@ -38,6 +38,7 @@ func TestGetConfigFromSpec(t *testing.T) { annotBlock: "block1", annotBlockMntPoint: "point1", annotMountRootfs: "true", + annotNetDev: "mynetdev", }, } @@ -50,6 +51,7 @@ func TestGetConfigFromSpec(t *testing.T) { Block: "block1", BlkMntPoint: "point1", MountRootfs: "true", + NetDev: "mynetdev", } config := getConfigFromSpec(spec) @@ -239,6 +241,7 @@ func TestMap(t *testing.T) { Block: "block_value", BlkMntPoint: "point_value", MountRootfs: "false", + NetDev: "netdev_value", } expectedMap := map[string]string{ annotCmdLine: "cmd_value", @@ -249,6 +252,7 @@ func TestMap(t *testing.T) { annotBlock: "block_value", annotBlockMntPoint: "point_value", annotMountRootfs: "false", + annotNetDev: "netdev_value", } resultMap := config.Map() assert.Equal(t, expectedMap, resultMap) diff --git a/pkg/unikontainers/types/types.go b/pkg/unikontainers/types/types.go index c6388e2cc..04f5204b4 100644 --- a/pkg/unikontainers/types/types.go +++ b/pkg/unikontainers/types/types.go @@ -86,6 +86,7 @@ type UnikernelParams struct { Monitor string // The monitor where guest will execute Version string // The version of the unikernel InitrdPath string // The path to the initrd of the unikernel + NetDevName string // The name of the guest network device declared at build time Net NetDevParams Block []BlockDevParams Rootfs RootfsParams // Information about rootfs diff --git a/pkg/unikontainers/unikernels/mirage.go b/pkg/unikontainers/unikernels/mirage.go index 133ded8fa..791176432 100644 --- a/pkg/unikontainers/unikernels/mirage.go +++ b/pkg/unikontainers/unikernels/mirage.go @@ -24,10 +24,11 @@ import ( const MirageUnikernel string = "mirage" type Mirage struct { - Command string - Monitor string - Net MirageNet - Block []MirageBlock + Command string + Monitor string + Net MirageNet + Block []MirageBlock + netDevName string } type MirageNet struct { @@ -57,8 +58,9 @@ func (m *Mirage) SupportsFS(_ string) bool { func (m *Mirage) MonitorNetCli(ifName string, mac string) string { switch m.Monitor { case "hvt", "spt": - netOption := "--net:service=" + ifName - netOption += " --net-mac:service=" + mac + name := m.netDevName + netOption := "--net:" + name + "=" + ifName + netOption += " --net-mac:" + name + "=" + mac return netOption default: return "" @@ -114,6 +116,12 @@ func (m *Mirage) Init(data types.UnikernelParams) error { m.Command = strings.Join(data.CmdLine, " ") m.Monitor = data.Monitor + if data.NetDevName != "" { + m.netDevName = data.NetDevName + } else { + m.netDevName = "service" + } + return nil } diff --git a/pkg/unikontainers/unikernels/mirage_test.go b/pkg/unikontainers/unikernels/mirage_test.go new file mode 100644 index 000000000..09225ca7a --- /dev/null +++ b/pkg/unikontainers/unikernels/mirage_test.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023-2026, 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 ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urunc-dev/urunc/pkg/unikontainers/types" +) + +func TestMirageNetDevName(t *testing.T) { + t.Run("uses net device name from annotation", func(t *testing.T) { + t.Parallel() + m := &Mirage{} + err := m.Init(types.UnikernelParams{Monitor: "hvt", NetDevName: "mynetdev"}) + assert.NoError(t, err) + cli := m.MonitorNetCli("tap0", "aa:bb:cc:dd:ee:ff") + assert.Contains(t, cli, "--net:mynetdev=tap0") + assert.Contains(t, cli, "--net-mac:mynetdev=aa:bb:cc:dd:ee:ff") + }) + + t.Run("falls back to service when annotation is absent", func(t *testing.T) { + t.Parallel() + m := &Mirage{} + err := m.Init(types.UnikernelParams{Monitor: "hvt"}) + assert.NoError(t, err) + cli := m.MonitorNetCli("tap0", "aa:bb:cc:dd:ee:ff") + assert.Contains(t, cli, "--net:service=tap0") + assert.Contains(t, cli, "--net-mac:service=aa:bb:cc:dd:ee:ff") + }) +} diff --git a/pkg/unikontainers/unikontainers.go b/pkg/unikontainers/unikontainers.go index c16a3a607..352352470 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -380,11 +380,12 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { // UnikernelParams // populate unikernel params unikernelParams := types.UnikernelParams{ - CmdLine: u.Spec.Process.Args, - EnvVars: u.Spec.Process.Env, - Monitor: vmmType, - Version: unikernelVersion, - ProcConf: procAttrs, + CmdLine: u.Spec.Process.Args, + EnvVars: u.Spec.Process.Env, + Monitor: vmmType, + Version: unikernelVersion, + ProcConf: procAttrs, + NetDevName: u.State.Annotations[annotNetDev], } if len(unikernelParams.CmdLine) == 0 { unikernelParams.CmdLine = strings.Fields(u.State.Annotations[annotCmdLine]) From 8d4d5c86dfee06e1338f2d89c4b41c6d42a8b4c0 Mon Sep 17 00:00:00 2001 From: viju Date: Wed, 20 May 2026 19:09:20 +0530 Subject: [PATCH 2/3] fix(mirage): prefix ipv4 options for non-default netDev Related: #315 Signed-off-by: viju --- pkg/unikontainers/unikernels/mirage.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/unikontainers/unikernels/mirage.go b/pkg/unikontainers/unikernels/mirage.go index 791176432..d047422dc 100644 --- a/pkg/unikontainers/unikernels/mirage.go +++ b/pkg/unikontainers/unikernels/mirage.go @@ -101,8 +101,13 @@ func (m *Mirage) MonitorCli() types.MonitorCliArgs { func (m *Mirage) Init(data types.UnikernelParams) error { // if Mask is empty, there is no network support if data.Net.Mask != "" { - m.Net.Address = "--ipv4=" + data.Net.IP + "/24" - m.Net.Gateway = "--ipv4-gateway=" + data.Net.Gateway + if data.NetDevName != "" && data.NetDevName != "service" { + m.Net.Address = "--" + data.NetDevName + "-ipv4=" + data.Net.IP + "/24" + m.Net.Gateway = "--" + data.NetDevName + "-ipv4-gateway=" + data.Net.Gateway + } else { + m.Net.Address = "--ipv4=" + data.Net.IP + "/24" + m.Net.Gateway = "--ipv4-gateway=" + data.Net.Gateway + } } m.Block = make([]MirageBlock, 0, len(data.Block)) for _, blk := range data.Block { From a6eebadee5ef691f29cf58fa6c6967796bb07036 Mon Sep 17 00:00:00 2001 From: viju Date: Tue, 2 Jun 2026 19:21:19 +0000 Subject: [PATCH 3/3] refactor: prefix Solo5 net device identifiers per review Signed-off-by: viju --- pkg/unikontainers/config.go | 22 +++++++------- pkg/unikontainers/config_test.go | 8 +++--- pkg/unikontainers/types/types.go | 12 ++++---- pkg/unikontainers/unikernels/mirage.go | 32 ++++++++++++--------- pkg/unikontainers/unikernels/mirage_test.go | 4 +-- pkg/unikontainers/unikontainers.go | 12 ++++---- 6 files changed, 48 insertions(+), 42 deletions(-) diff --git a/pkg/unikontainers/config.go b/pkg/unikontainers/config.go index 21e239cf7..521a1f14b 100644 --- a/pkg/unikontainers/config.go +++ b/pkg/unikontainers/config.go @@ -45,7 +45,7 @@ const ( annotBlock = "com.urunc.unikernel.block" annotBlockMntPoint = "com.urunc.unikernel.blkMntPoint" annotMountRootfs = "com.urunc.unikernel.mountRootfs" - annotNetDev = "com.urunc.unikernel.netDev" + annotSolo5NetDev = "com.urunc.unikernel.netDev" ) // A UnikernelConfig struct holds the info provided by bima image on how to execute our unikernel @@ -59,7 +59,7 @@ type UnikernelConfig struct { Block string `json:"com.urunc.unikernel.block,omitempty"` BlkMntPoint string `json:"com.urunc.unikernel.blkMntPoint,omitempty"` MountRootfs string `json:"com.urunc.unikernel.mountRootfs"` - NetDev string `json:"com.urunc.unikernel.netDev,omitempty"` + Solo5NetDev string `json:"com.urunc.unikernel.netDev,omitempty"` } // validate checks if the mandatory configuration fields are present. @@ -129,7 +129,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { block := spec.Annotations[annotBlock] blkMntPoint := spec.Annotations[annotBlockMntPoint] MountRootfs := spec.Annotations[annotMountRootfs] - netDev := spec.Annotations[annotNetDev] + solo5NetDev := spec.Annotations[annotSolo5NetDev] uniklog.WithFields(logrus.Fields{ "unikernelType": tryDecode(unikernelType), "unikernelVersion": tryDecode(unikernelVersion), @@ -140,7 +140,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { "block": tryDecode(block), "blkMntPoint": tryDecode(blkMntPoint), "mountRootfs": tryDecode(MountRootfs), - "netDev": tryDecode(netDev), + "solo5NetDev": tryDecode(solo5NetDev), }).WithField("source", "spec").Debug("urunc annotations") return &UnikernelConfig{ @@ -153,7 +153,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { Block: block, BlkMntPoint: blkMntPoint, MountRootfs: MountRootfs, - NetDev: netDev, + Solo5NetDev: solo5NetDev, } } @@ -193,7 +193,7 @@ func getConfigFromJSON(jsonFilePath string) (*UnikernelConfig, error) { "block": tryDecode(conf.Block), "blkMntPoint": tryDecode(conf.BlkMntPoint), "mountRootfs": tryDecode(conf.MountRootfs), - "netDev": tryDecode(conf.NetDev), + "solo5NetDev": tryDecode(conf.Solo5NetDev), }).WithField("source", uruncJSONFilename).Debug("urunc annotations") return &conf, nil @@ -264,11 +264,11 @@ func (c *UnikernelConfig) decode() error { } c.MountRootfs = string(decoded) - decoded, err = base64.StdEncoding.DecodeString(c.NetDev) + decoded, err = base64.StdEncoding.DecodeString(c.Solo5NetDev) if err != nil { - return fmt.Errorf("failed to decode netDev: %v", err) + return fmt.Errorf("failed to decode solo5NetDev: %v", err) } - c.NetDev = string(decoded) + c.Solo5NetDev = string(decoded) return nil } @@ -303,8 +303,8 @@ func (c *UnikernelConfig) Map() map[string]string { if c.MountRootfs != "" { myMap[annotMountRootfs] = c.MountRootfs } - if c.NetDev != "" { - myMap[annotNetDev] = c.NetDev + if c.Solo5NetDev != "" { + myMap[annotSolo5NetDev] = c.Solo5NetDev } return myMap diff --git a/pkg/unikontainers/config_test.go b/pkg/unikontainers/config_test.go index 71a9eb5c7..5b7d540e8 100644 --- a/pkg/unikontainers/config_test.go +++ b/pkg/unikontainers/config_test.go @@ -38,7 +38,7 @@ func TestGetConfigFromSpec(t *testing.T) { annotBlock: "block1", annotBlockMntPoint: "point1", annotMountRootfs: "true", - annotNetDev: "mynetdev", + annotSolo5NetDev: "mynetdev", }, } @@ -51,7 +51,7 @@ func TestGetConfigFromSpec(t *testing.T) { Block: "block1", BlkMntPoint: "point1", MountRootfs: "true", - NetDev: "mynetdev", + Solo5NetDev: "mynetdev", } config := getConfigFromSpec(spec) @@ -241,7 +241,7 @@ func TestMap(t *testing.T) { Block: "block_value", BlkMntPoint: "point_value", MountRootfs: "false", - NetDev: "netdev_value", + Solo5NetDev: "netdev_value", } expectedMap := map[string]string{ annotCmdLine: "cmd_value", @@ -252,7 +252,7 @@ func TestMap(t *testing.T) { annotBlock: "block_value", annotBlockMntPoint: "point_value", annotMountRootfs: "false", - annotNetDev: "netdev_value", + annotSolo5NetDev: "netdev_value", } resultMap := config.Map() assert.Equal(t, expectedMap, resultMap) diff --git a/pkg/unikontainers/types/types.go b/pkg/unikontainers/types/types.go index 04f5204b4..c5971f8a0 100644 --- a/pkg/unikontainers/types/types.go +++ b/pkg/unikontainers/types/types.go @@ -86,11 +86,13 @@ type UnikernelParams struct { Monitor string // The monitor where guest will execute Version string // The version of the unikernel InitrdPath string // The path to the initrd of the unikernel - NetDevName string // The name of the guest network device declared at build time - Net NetDevParams - Block []BlockDevParams - Rootfs RootfsParams // Information about rootfs - ProcConf ProcessConfig // Information for the process execution inside the guest + // Solo5NetDevName is the Solo5 network device name declared in the + // unikernel manifest at build time. + Solo5NetDevName string + Net NetDevParams + Block []BlockDevParams + Rootfs RootfsParams // Information about rootfs + ProcConf ProcessConfig // Information for the process execution inside the guest } // ExecArgs holds the data required by Execve to start the VMM diff --git a/pkg/unikontainers/unikernels/mirage.go b/pkg/unikontainers/unikernels/mirage.go index d047422dc..81412ea23 100644 --- a/pkg/unikontainers/unikernels/mirage.go +++ b/pkg/unikontainers/unikernels/mirage.go @@ -24,11 +24,11 @@ import ( const MirageUnikernel string = "mirage" type Mirage struct { - Command string - Monitor string - Net MirageNet - Block []MirageBlock - netDevName string + Command string + Monitor string + Net MirageNet + Block []MirageBlock + solo5NetDevName string } type MirageNet struct { @@ -58,9 +58,8 @@ func (m *Mirage) SupportsFS(_ string) bool { func (m *Mirage) MonitorNetCli(ifName string, mac string) string { switch m.Monitor { case "hvt", "spt": - name := m.netDevName - netOption := "--net:" + name + "=" + ifName - netOption += " --net-mac:" + name + "=" + mac + netOption := "--net:" + m.solo5NetDevName + "=" + ifName + netOption += " --net-mac:" + m.solo5NetDevName + "=" + mac return netOption default: return "" @@ -101,9 +100,14 @@ func (m *Mirage) MonitorCli() types.MonitorCliArgs { func (m *Mirage) Init(data types.UnikernelParams) error { // if Mask is empty, there is no network support if data.Net.Mask != "" { - if data.NetDevName != "" && data.NetDevName != "service" { - m.Net.Address = "--" + data.NetDevName + "-ipv4=" + data.Net.IP + "/24" - m.Net.Gateway = "--" + data.NetDevName + "-ipv4-gateway=" + data.Net.Gateway + // Mirage groups a network device's command line options under a prefix, + // and that prefix is the device name. The default device "service" uses + // the plain --ipv4 and --ipv4-gateway flags, but any other device needs + // its name as the prefix, like --management-ipv4 for a device called + // "management". So we only add the prefix when the device is not "service". + if data.Solo5NetDevName != "" && data.Solo5NetDevName != "service" { + m.Net.Address = "--" + data.Solo5NetDevName + "-ipv4=" + data.Net.IP + "/24" + m.Net.Gateway = "--" + data.Solo5NetDevName + "-ipv4-gateway=" + data.Net.Gateway } else { m.Net.Address = "--ipv4=" + data.Net.IP + "/24" m.Net.Gateway = "--ipv4-gateway=" + data.Net.Gateway @@ -121,10 +125,10 @@ func (m *Mirage) Init(data types.UnikernelParams) error { m.Command = strings.Join(data.CmdLine, " ") m.Monitor = data.Monitor - if data.NetDevName != "" { - m.netDevName = data.NetDevName + if data.Solo5NetDevName != "" { + m.solo5NetDevName = data.Solo5NetDevName } else { - m.netDevName = "service" + m.solo5NetDevName = "service" } return nil diff --git a/pkg/unikontainers/unikernels/mirage_test.go b/pkg/unikontainers/unikernels/mirage_test.go index 09225ca7a..77f2aee57 100644 --- a/pkg/unikontainers/unikernels/mirage_test.go +++ b/pkg/unikontainers/unikernels/mirage_test.go @@ -21,11 +21,11 @@ import ( "github.com/urunc-dev/urunc/pkg/unikontainers/types" ) -func TestMirageNetDevName(t *testing.T) { +func TestMirageSolo5NetDevName(t *testing.T) { t.Run("uses net device name from annotation", func(t *testing.T) { t.Parallel() m := &Mirage{} - err := m.Init(types.UnikernelParams{Monitor: "hvt", NetDevName: "mynetdev"}) + err := m.Init(types.UnikernelParams{Monitor: "hvt", Solo5NetDevName: "mynetdev"}) assert.NoError(t, err) cli := m.MonitorNetCli("tap0", "aa:bb:cc:dd:ee:ff") assert.Contains(t, cli, "--net:mynetdev=tap0") diff --git a/pkg/unikontainers/unikontainers.go b/pkg/unikontainers/unikontainers.go index 352352470..d1176b00f 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -380,12 +380,12 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { // UnikernelParams // populate unikernel params unikernelParams := types.UnikernelParams{ - CmdLine: u.Spec.Process.Args, - EnvVars: u.Spec.Process.Env, - Monitor: vmmType, - Version: unikernelVersion, - ProcConf: procAttrs, - NetDevName: u.State.Annotations[annotNetDev], + CmdLine: u.Spec.Process.Args, + EnvVars: u.Spec.Process.Env, + Monitor: vmmType, + Version: unikernelVersion, + ProcConf: procAttrs, + Solo5NetDevName: u.State.Annotations[annotSolo5NetDev], } if len(unikernelParams.CmdLine) == 0 { unikernelParams.CmdLine = strings.Fields(u.State.Annotations[annotCmdLine])