From 9b6eb01166e1460500af778d458580201eb39a7e Mon Sep 17 00:00:00 2001 From: Andrey P Date: Tue, 11 Aug 2026 21:25:10 +0100 Subject: [PATCH] nic: allow create-path NIC devices with nictype and no managed network toNicDevice() unconditionally required a managed `network:` (via Config.Network or Config.NetworkName), even though Extensions already carries everything an unmanaged-bridge NIC device needs (nictype + parent). Incus itself accepts this device shape directly -- the validation here was stricter than the daemon it targets. Only reject when neither a managed network nor `nictype` is present, matching Incus's own two valid ways to declare a NIC. network: is also no longer forced into the device map when unset, so `nictype` based devices don't pick up an empty network="" key. Adds TestNicDevices (regression case fails against the prior code with the exact reported error, "bad config for device eth1") and a narrow- scoping error case confirming an unrelated extension alone still errors. Signed-off-by: Andrey P --- client/resource_instance_device.go | 14 +++-- client/resource_instance_device_test.go | 54 +++++++++++++++++++ .../client/TestNicDevices-nic_managed_network | 8 +++ .../TestNicDevices-nic_unmanaged_bridge | 9 ++++ 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 test/snapshots/client/TestNicDevices-nic_managed_network create mode 100644 test/snapshots/client/TestNicDevices-nic_unmanaged_bridge diff --git a/client/resource_instance_device.go b/client/resource_instance_device.go index edaadbb9..1fc1a89a 100644 --- a/client/resource_instance_device.go +++ b/client/resource_instance_device.go @@ -141,14 +141,20 @@ func (d *InstanceDevice) toNicDevice() (map[string]string, error) { networkName = d.Config.Network.IncusName() } else if d.Config.NetworkName != "" { networkName = d.Config.NetworkName - } else { + } else if _, hasNicType := d.Config.Extensions["nictype"]; !hasNicType { + // A managed `network:` isn't the only valid way to declare a NIC -- Incus itself accepts + // a device carrying `nictype` (e.g. "bridged" + "parent") with no `network` key at all, + // which is the only way to attach to an unmanaged host bridge (MANAGED=NO in `incus + // network list`). Only reject when neither a managed network nor nictype is present. return map[string]string{}, ErrBadDeviceConfig.WithText("network not given") } device := map[string]string{ - "type": "nic", - "name": d.Name, - "network": networkName, + "type": "nic", + "name": d.Name, + } + if networkName != "" { + device["network"] = networkName } maps.Copy(device, d.Config.Extensions) diff --git a/client/resource_instance_device_test.go b/client/resource_instance_device_test.go index 1add4da8..38e8d7ff 100644 --- a/client/resource_instance_device_test.go +++ b/client/resource_instance_device_test.go @@ -221,6 +221,43 @@ func TestDiskDevices(t *testing.T) { } } +func TestNicDevices(t *testing.T) { + t.Parallel() + + testCases := []DeviceTestCase{ + { + Name: "nic_managed_network", + Device: InstanceDevice{ + Name: "eth0", + Config: InstanceDeviceConfig{ + DeviceType: InstanceDeviceTypeNic, + NetworkName: "incusbr0", + }, + }, + }, + { + // Regression test: an unmanaged host bridge (nictype + parent, no + // managed network) must be passed through, not rejected. Incus + // itself accepts this device shape directly. + Name: "nic_unmanaged_bridge", + Device: InstanceDevice{ + Name: "eth1", + Config: InstanceDeviceConfig{ + DeviceType: InstanceDeviceTypeNic, + Extensions: map[string]string{ + "nictype": "bridged", + "parent": "br0", + }, + }, + }, + }, + } + + for _, tc := range testCases { + runDeviceTest(t, tc) + } +} + func TestTmpfsDevices(t *testing.T) { t.Parallel() @@ -272,6 +309,23 @@ func TestDeviceErrors(t *testing.T) { require.Error(t, err) }) + // The nictype exemption is narrow: an unrelated extension must not also + // satisfy it. + t.Run("nic_no_network_unrelated_extension", func(t *testing.T) { + t.Parallel() + device := InstanceDevice{ + Name: "eth0", + Config: InstanceDeviceConfig{ + DeviceType: InstanceDeviceTypeNic, + Extensions: map[string]string{ + "mtu": "1500", + }, + }, + } + _, _, err := device.ToIncusDevice() + require.Error(t, err) + }) + t.Run("unknown_no_extra", func(t *testing.T) { t.Parallel() device := InstanceDevice{ diff --git a/test/snapshots/client/TestNicDevices-nic_managed_network b/test/snapshots/client/TestNicDevices-nic_managed_network new file mode 100644 index 00000000..67473353 --- /dev/null +++ b/test/snapshots/client/TestNicDevices-nic_managed_network @@ -0,0 +1,8 @@ +{ + "config": { + "name": "eth0", + "network": "incusbr0", + "type": "nic" + }, + "name": "eth0" +} diff --git a/test/snapshots/client/TestNicDevices-nic_unmanaged_bridge b/test/snapshots/client/TestNicDevices-nic_unmanaged_bridge new file mode 100644 index 00000000..29dd74e9 --- /dev/null +++ b/test/snapshots/client/TestNicDevices-nic_unmanaged_bridge @@ -0,0 +1,9 @@ +{ + "config": { + "name": "eth1", + "nictype": "bridged", + "parent": "br0", + "type": "nic" + }, + "name": "eth1" +}