From f408a1f02c3aed7368f9a097a6f4640c497a976d Mon Sep 17 00:00:00 2001 From: Hemanth kumar Pannem Date: Mon, 1 Dec 2025 16:35:43 -0800 Subject: [PATCH] ipv6 fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PR fixes 3 issues and adds comprehensive tests for IPv6 support. What does this PR do, and why is it needed? Issue1: Fix Critical Bug: IPv6-Only GOSC Customization When only IPv6 is configured (no IPv4 addresses, no DHCP4), the adapter.Ip field remains nil, causing vSphere API to reject the customization spec. Cause The error occurs when customizing a VM with IPv6-only network configuration (no IPv4). The vSphere API requires the ip field in CustomizationIPSettings to be present, even when using IPv6. Error Message Required property ip is missing from data object of type CustomizationIPSettings while parsing serialized DataObject of type vim.vm.customization.IPSettings Error Flow VM customization is triggered via doCustomize() in bootstrap.go:353 Customize() is called on the VM object (resources/vm.go:225) vSphere API receives the customization spec with NicSettingMap For the second adapter (IPv6-only), the spec has: { "macAddress": "00:50:56:96:8f:c8", "adapter": { "ip": null, // ❌ This is the problem "ipV6Spec": { "ip": [{"ipAddress": "2001:db8::100", "subnetMask": 64}], "gateway": ["2001:db8::1"] } } } vSphere API rejects it because ip field is required File: pkg/providers/vsphere/network/gosc.go:22-49 Fix Required: After the IPv4 switch statement (after line 49), check if adapter.Ip is still nil and IPv6 is configured. If so, set adapter.Ip to: &vimtypes.CustomizationDhcpIpGenerator{} (fallback - allows IPv4 DHCP but won't fail if IPv4 unavailable) Issue2: Trivial: Update Outdated Comment Current Code: File: pkg/providers/vsphere/network/network.go:206-210 if interfaceSpec.DHCP6 { // We don't really support IPv6 yet so this is only enabled when specified in // the interface spec. result.DHCP6 = true } Issue3: Handle IPv6-Only Scenarios in NetOP File: pkg/providers/vsphere/network/network.go:461-475 Current Logic When IPAssignmentMode == DHCP, sets DHCP4 = true regardless of available IP families When IPAssignmentMode == StaticPool, processes all IPConfigs (both IPv4 and IPv6) Fix DHCP flag on the NetworkInterface indicates that dhcp4 and dhcp6 are enabled. Add Comprehensive Tests IPv6-only from NetOP (no IPv4 addresses) IPv6-only GOSC customization (critical bug fix) DHCP mode with IPv6 addresses User-specified IPv6 addresses with NetOP-provided IPv4 User-specified IPv6 addresses only IPv6 gateway handling (from NetOP, from user spec, "None" value) Dual-stack with different gateways for IPv4 and IPv6 --- pkg/providers/vsphere/network/gosc.go | 5 + pkg/providers/vsphere/network/gosc_test.go | 74 +++ pkg/providers/vsphere/network/netplan_test.go | 159 ++++++ pkg/providers/vsphere/network/network.go | 15 +- pkg/providers/vsphere/network/network_test.go | 539 +++++++++++++++++- 5 files changed, 789 insertions(+), 3 deletions(-) diff --git a/pkg/providers/vsphere/network/gosc.go b/pkg/providers/vsphere/network/gosc.go index 1cab2dc91..808010c23 100644 --- a/pkg/providers/vsphere/network/gosc.go +++ b/pkg/providers/vsphere/network/gosc.go @@ -46,6 +46,11 @@ func GuestOSCustomization(results NetworkInterfaceResults) ([]vimtypes.Customiza break } + // When adapter.Ip is nil, the vSphere API requires it to be set. + // Set it to disable IPv4, which handles both IPv6-only and completely unconfigured cases. + if adapter.Ip == nil { + adapter.Ip = &vimtypes.CustomizationDisableIpV4{} + } } switch { diff --git a/pkg/providers/vsphere/network/gosc_test.go b/pkg/providers/vsphere/network/gosc_test.go index 69c81c805..e0b035c75 100644 --- a/pkg/providers/vsphere/network/gosc_test.go +++ b/pkg/providers/vsphere/network/gosc_test.go @@ -185,5 +185,79 @@ var _ = Describe("GOSC", func() { Expect(adapter.IpV6Spec).To(BeNil()) }) }) + + Context("Unconfigured interface", func() { + BeforeEach(func() { + results.Results = []network.NetworkInterfaceResult{ + { + MacAddress: macAddr1, + Name: "eth0", + IPConfigs: []network.NetworkInterfaceIPConfig{}, + DHCP4: false, + DHCP6: false, + NoIPAM: false, + MTU: 1500, + Nameservers: []string{dnsServer1}, + }, + } + }) + + It("returns success", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(adapterMappings).To(HaveLen(1)) + mapping := adapterMappings[0] + + adapter := mapping.Adapter + Expect(mapping.MacAddress).To(Equal(macAddr1)) + // Unconfigured interface fix: adapter.Ip should be set to disable IPv4 + // This matches Linux behavior where an interface can exist without an IP address + Expect(adapter.Ip).To(BeAssignableToTypeOf(&vimtypes.CustomizationDisableIpV4{})) + Expect(adapter.IpV6Spec).To(BeNil()) + Expect(adapter.Gateway).To(BeEmpty()) + Expect(adapter.SubnetMask).To(BeEmpty()) + Expect(adapter.DnsServerList).To(Equal([]string{dnsServer1})) + }) + }) + + Context("IPv6-Only Static", func() { + BeforeEach(func() { + results.Results = []network.NetworkInterfaceResult{ + { + IPConfigs: []network.NetworkInterfaceIPConfig{ + { + IPCIDR: "2001:db8::100/64", + IsIPv4: false, + Gateway: "2001:db8::1", + }, + }, + MacAddress: macAddr1, + Name: "eth0", + DHCP4: false, + DHCP6: false, + MTU: 1500, + Nameservers: []string{"2001:4860:4860::8888"}, + }, + } + }) + + It("returns success", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(adapterMappings).To(HaveLen(1)) + mapping := adapterMappings[0] + + adapter := mapping.Adapter + Expect(mapping.MacAddress).To(Equal(macAddr1)) + // IPv6-only fix: adapter.Ip should be set to disable IPv4 + Expect(adapter.Ip).To(BeAssignableToTypeOf(&vimtypes.CustomizationDisableIpV4{})) + Expect(adapter.IpV6Spec).ToNot(BeNil()) + Expect(adapter.IpV6Spec.Gateway).To(Equal([]string{"2001:db8::1"})) + Expect(adapter.IpV6Spec.Ip).To(HaveLen(1)) + Expect(adapter.IpV6Spec.Ip[0]).To(BeAssignableToTypeOf(&vimtypes.CustomizationFixedIpV6{})) + addressSpec := adapter.IpV6Spec.Ip[0].(*vimtypes.CustomizationFixedIpV6) + Expect(addressSpec.IpAddress).To(Equal("2001:db8::100")) + Expect(addressSpec.SubnetMask).To(BeEquivalentTo(64)) + }) + }) + }) }) diff --git a/pkg/providers/vsphere/network/netplan_test.go b/pkg/providers/vsphere/network/netplan_test.go index d03fd4408..c5406309c 100644 --- a/pkg/providers/vsphere/network/netplan_test.go +++ b/pkg/providers/vsphere/network/netplan_test.go @@ -245,5 +245,164 @@ var _ = Describe("Netplan", func() { Expect(np.AcceptRa).To(HaveValue(BeFalse())) }) }) + + Context("IPv4-Only Static", func() { + BeforeEach(func() { + results.Results = []network.NetworkInterfaceResult{ + { + IPConfigs: []network.NetworkInterfaceIPConfig{ + { + IPCIDR: "192.168.1.100/24", + IsIPv4: true, + Gateway: "192.168.1.1", + }, + }, + MacAddress: macAddr1, + Name: ifName, + GuestDeviceName: guestDevName, + DHCP4: false, + DHCP6: false, + MTU: 1500, + Nameservers: []string{dnsServer1}, + }, + } + }) + + It("returns success with IPv4-only configuration", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(config).ToNot(BeNil()) + + np := config.Ethernets[ifName] + Expect(np.Addresses).To(HaveLen(1)) + Expect(np.Addresses[0]).To(Equal(netplan.Address{String: ptr.To("192.168.1.100/24")})) + Expect(np.Gateway4).To(HaveValue(Equal("192.168.1.1"))) + Expect(np.Gateway6).To(BeNil()) + Expect(np.Dhcp4).To(HaveValue(BeFalse())) + Expect(np.Dhcp6).To(HaveValue(BeFalse())) + }) + }) + + Context("IPv6-Only Static", func() { + BeforeEach(func() { + results.Results = []network.NetworkInterfaceResult{ + { + IPConfigs: []network.NetworkInterfaceIPConfig{ + { + IPCIDR: "2001:db8::100/64", + IsIPv4: false, + Gateway: "2001:db8::1", + }, + }, + MacAddress: macAddr1, + Name: ifName, + GuestDeviceName: guestDevName, + DHCP4: false, + DHCP6: false, + MTU: 1500, + Nameservers: []string{"2001:4860:4860::8888"}, + }, + } + }) + + It("returns success with IPv6-only configuration", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(config).ToNot(BeNil()) + + np := config.Ethernets[ifName] + Expect(np.Addresses).To(HaveLen(1)) + Expect(np.Addresses[0]).To(Equal(netplan.Address{String: ptr.To("2001:db8::100/64")})) + Expect(np.Gateway4).To(BeNil()) + Expect(np.Gateway6).To(HaveValue(Equal("2001:db8::1"))) + Expect(np.Dhcp4).To(HaveValue(BeFalse())) + Expect(np.Dhcp6).To(HaveValue(BeFalse())) + }) + }) + + Context("Multiple IPv6 Addresses", func() { + BeforeEach(func() { + results.Results = []network.NetworkInterfaceResult{ + { + IPConfigs: []network.NetworkInterfaceIPConfig{ + { + IPCIDR: "2001:db8::100/64", + IsIPv4: false, + Gateway: "2001:db8::1", + }, + { + IPCIDR: "2001:db8::101/64", + IsIPv4: false, + Gateway: "2001:db8::1", + }, + }, + MacAddress: macAddr1, + Name: ifName, + GuestDeviceName: guestDevName, + DHCP4: false, + DHCP6: false, + MTU: 1500, + Nameservers: []string{"2001:4860:4860::8888"}, + }, + } + }) + + It("returns success with multiple IPv6 addresses", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(config).ToNot(BeNil()) + Expect(config.Version).To(Equal(constants.NetPlanVersion)) + + Expect(config.Ethernets).To(HaveLen(1)) + Expect(config.Ethernets).To(HaveKey(ifName)) + + np := config.Ethernets[ifName] + Expect(np.Addresses).To(HaveLen(2)) + Expect(np.Addresses[0]).To(Equal(netplan.Address{String: ptr.To("2001:db8::100/64")})) + Expect(np.Addresses[1]).To(Equal(netplan.Address{String: ptr.To("2001:db8::101/64")})) + Expect(np.Gateway6).To(HaveValue(Equal("2001:db8::1"))) + }) + }) + + Context("Multiple IPv4 Addresses", func() { + BeforeEach(func() { + results.Results = []network.NetworkInterfaceResult{ + { + IPConfigs: []network.NetworkInterfaceIPConfig{ + { + IPCIDR: "192.168.1.100/24", + IsIPv4: true, + Gateway: "192.168.1.1", + }, + { + IPCIDR: "192.168.1.101/24", + IsIPv4: true, + Gateway: "192.168.1.1", + }, + }, + MacAddress: macAddr1, + Name: ifName, + GuestDeviceName: guestDevName, + DHCP4: false, + DHCP6: false, + MTU: 1500, + Nameservers: []string{dnsServer1}, + }, + } + }) + + It("returns success with multiple IPv4 addresses", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(config).ToNot(BeNil()) + Expect(config.Version).To(Equal(constants.NetPlanVersion)) + + Expect(config.Ethernets).To(HaveLen(1)) + Expect(config.Ethernets).To(HaveKey(ifName)) + + np := config.Ethernets[ifName] + Expect(np.Addresses).To(HaveLen(2)) + Expect(np.Addresses[0]).To(Equal(netplan.Address{String: ptr.To("192.168.1.100/24")})) + Expect(np.Addresses[1]).To(Equal(netplan.Address{String: ptr.To("192.168.1.101/24")})) + Expect(np.Gateway4).To(HaveValue(Equal("192.168.1.1"))) + }) + }) + }) }) diff --git a/pkg/providers/vsphere/network/network.go b/pkg/providers/vsphere/network/network.go index 47fd90193..d69dde5e8 100644 --- a/pkg/providers/vsphere/network/network.go +++ b/pkg/providers/vsphere/network/network.go @@ -203,12 +203,14 @@ func applyInterfaceSpecToResult( result.MTU = *interfaceSpec.MTU } + // TODO: There is currently no way to unset DHCP flags if NetOP has set them. + // For example, if NetOP sets both DHCP4 and DHCP6, but user only wants DHCP4, + // they cannot disable DHCP6. Consider adding explicit unset mechanism (e.g., + // using pointer types or separate "disable" flags) in the future. if interfaceSpec.DHCP4 { result.DHCP4 = true } if interfaceSpec.DHCP6 { - // We don't really support IPv6 yet so this is only enabled when specified in - // the interface spec. result.DHCP6 = true } @@ -463,10 +465,19 @@ func netOpNetIfToResult( switch netIf.Status.IPAssignmentMode { case netopv1alpha1.NetworkInterfaceIPAssignmentModeDHCP: + // When NetOP indicates DHCP, IPConfigs will be empty. + // Since NetOP doesn't distinguish between IPv4 and IPv6, set both. + // User's interface spec can override either or both flags. result.DHCP4 = true + result.DHCP6 = true case netopv1alpha1.NetworkInterfaceIPAssignmentModeNone: result.NoIPAM = true default: // netopv1alpha1.NetworkInterfaceIPAssignmentModeStaticPool + // Process all IPConfigs (both IPv4 and IPv6). This correctly handles: + // - IPv4-only scenarios (only IPv4 IPConfigs) + // - IPv6-only scenarios (only IPv6 IPConfigs) + // - Dual-stack scenarios (both IPv4 and IPv6 IPConfigs) + // DHCP4/DHCP6 are not set in StaticPool mode, only IPConfigs are populated. for _, ip := range netIf.Status.IPConfigs { ipConfig := NetworkInterfaceIPConfig{ IPCIDR: ipCIDRNotation(ip.IP, ip.SubnetMask, ip.IPFamily == corev1.IPv4Protocol), diff --git a/pkg/providers/vsphere/network/network_test.go b/pkg/providers/vsphere/network/network_test.go index 014091c42..49dbf9322 100644 --- a/pkg/providers/vsphere/network/network_test.go +++ b/pkg/providers/vsphere/network/network_test.go @@ -462,7 +462,7 @@ var _ = Describe("CreateAndWaitForNetworkInterfaces", Label(testlabels.VCSim), f Expect(results.Results).To(HaveLen(1)) result := results.Results[0] Expect(result.DHCP4).To(BeTrue()) - Expect(result.DHCP6).To(BeFalse()) + Expect(result.DHCP6).To(BeTrue()) // Both should be set when NetOP indicates DHCP Expect(result.NoIPAM).To(BeFalse()) Expect(result.IPConfigs).To(BeEmpty()) }) @@ -603,6 +603,543 @@ var _ = Describe("CreateAndWaitForNetworkInterfaces", Label(testlabels.VCSim), f }) }) }) + + Context("Multiple IPv6 Addresses StaticPool from NetOP", func() { + BeforeEach(func() { + networkSpec.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName, + Network: &common.PartialObjectRef{ + Name: networkName, + }, + }, + } + }) + + It("returns success with multiple IPv6 addresses", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + + By("simulate successful NetOP reconcile with multiple IPv6", func() { + netInterface := &netopv1alpha1.NetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) + netInterface.Status.NetworkID = ctx.NetworkRef.Reference().Value + netInterface.Status.IPAssignmentMode = netopv1alpha1.NetworkInterfaceIPAssignmentModeStaticPool + netInterface.Status.IPConfigs = []netopv1alpha1.IPConfig{ + { + IP: "2001:db8::100", + IPFamily: corev1.IPv6Protocol, + Gateway: "2001:db8::1", + SubnetMask: "ffff:ffff:ffff:ffff::", + }, + { + IP: "2001:db8::101", + IPFamily: corev1.IPv6Protocol, + Gateway: "2001:db8::1", + SubnetMask: "ffff:ffff:ffff:ffff::", + }, + } + netInterface.Status.Conditions = []netopv1alpha1.NetworkInterfaceCondition{ + { + Type: netopv1alpha1.NetworkInterfaceReady, + Status: corev1.ConditionTrue, + }, + } + Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) + }) + + results, err = network.CreateAndWaitForNetworkInterfaces( + vmCtx, + ctx.Client, + ctx.VCClient.Client, + ctx.Finder, + nil, + networkSpec) + Expect(err).ToNot(HaveOccurred()) + + Expect(results.Results).To(HaveLen(1)) + result := results.Results[0] + Expect(result.IPConfigs).To(HaveLen(2)) + Expect(result.IPConfigs[0].IPCIDR).To(Equal("2001:db8::100/64")) + Expect(result.IPConfigs[1].IPCIDR).To(Equal("2001:db8::101/64")) + }) + }) + + Context("Multiple IPv4 Addresses StaticPool from NetOP", func() { + BeforeEach(func() { + networkSpec.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName, + Network: &common.PartialObjectRef{ + Name: networkName, + }, + }, + } + }) + + It("returns success with multiple IPv4 addresses", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + + By("simulate successful NetOP reconcile with multiple IPv4", func() { + netInterface := &netopv1alpha1.NetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) + netInterface.Status.NetworkID = ctx.NetworkRef.Reference().Value + netInterface.Status.IPAssignmentMode = netopv1alpha1.NetworkInterfaceIPAssignmentModeStaticPool + netInterface.Status.IPConfigs = []netopv1alpha1.IPConfig{ + { + IP: "192.168.1.100", + IPFamily: corev1.IPv4Protocol, + Gateway: "192.168.1.1", + SubnetMask: "255.255.255.0", + }, + { + IP: "192.168.1.101", + IPFamily: corev1.IPv4Protocol, + Gateway: "192.168.1.1", + SubnetMask: "255.255.255.0", + }, + } + netInterface.Status.Conditions = []netopv1alpha1.NetworkInterfaceCondition{ + { + Type: netopv1alpha1.NetworkInterfaceReady, + Status: corev1.ConditionTrue, + }, + } + Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) + }) + + results, err = network.CreateAndWaitForNetworkInterfaces( + vmCtx, + ctx.Client, + ctx.VCClient.Client, + ctx.Finder, + nil, + networkSpec) + Expect(err).ToNot(HaveOccurred()) + + Expect(results.Results).To(HaveLen(1)) + result := results.Results[0] + Expect(result.IPConfigs).To(HaveLen(2)) + Expect(result.IPConfigs[0].IPCIDR).To(Equal("192.168.1.100/24")) + Expect(result.IPConfigs[1].IPCIDR).To(Equal("192.168.1.101/24")) + }) + }) + + Context("User Addresses Override NetOP StaticPool", func() { + BeforeEach(func() { + networkSpec.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName, + Network: &common.PartialObjectRef{ + Name: networkName, + }, + Addresses: []string{"192.168.1.100/24", "2001:db8::100/64"}, + }, + } + }) + + It("returns success with user addresses overriding NetOP", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + + By("simulate successful NetOP reconcile", func() { + netInterface := &netopv1alpha1.NetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) + netInterface.Status.NetworkID = ctx.NetworkRef.Reference().Value + netInterface.Status.IPAssignmentMode = netopv1alpha1.NetworkInterfaceIPAssignmentModeStaticPool + netInterface.Status.IPConfigs = []netopv1alpha1.IPConfig{ + { + IP: "10.0.0.100", + IPFamily: corev1.IPv4Protocol, + Gateway: "10.0.0.1", + SubnetMask: "255.255.255.0", + }, + } + netInterface.Status.Conditions = []netopv1alpha1.NetworkInterfaceCondition{ + { + Type: netopv1alpha1.NetworkInterfaceReady, + Status: corev1.ConditionTrue, + }, + } + Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) + }) + + results, err = network.CreateAndWaitForNetworkInterfaces( + vmCtx, + ctx.Client, + ctx.VCClient.Client, + ctx.Finder, + nil, + networkSpec) + Expect(err).ToNot(HaveOccurred()) + + Expect(results.Results).To(HaveLen(1)) + result := results.Results[0] + Expect(result.IPConfigs).To(HaveLen(2)) + // User addresses should override NetOP addresses + Expect(result.IPConfigs[0].IPCIDR).To(Equal("192.168.1.100/24")) + Expect(result.IPConfigs[1].IPCIDR).To(Equal("2001:db8::100/64")) + // Gateway4 should be backfilled from NetOP + Expect(result.IPConfigs[0].Gateway).To(Equal("10.0.0.1")) + }) + }) + + Context("User Gateways Override NetOP Gateways", func() { + BeforeEach(func() { + networkSpec.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName, + Network: &common.PartialObjectRef{ + Name: networkName, + }, + Addresses: []string{"192.168.1.100/24", "2001:db8::100/64"}, + Gateway4: "172.16.1.1", + Gateway6: "2001:db8::2", + }, + } + }) + + It("returns success with user gateways overriding NetOP", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + + By("simulate successful NetOP reconcile", func() { + netInterface := &netopv1alpha1.NetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) + netInterface.Status.NetworkID = ctx.NetworkRef.Reference().Value + netInterface.Status.IPAssignmentMode = netopv1alpha1.NetworkInterfaceIPAssignmentModeStaticPool + netInterface.Status.IPConfigs = []netopv1alpha1.IPConfig{ + { + IP: "192.168.1.100", + IPFamily: corev1.IPv4Protocol, + Gateway: "192.168.1.1", + SubnetMask: "255.255.255.0", + }, + { + IP: "2001:db8::100", + IPFamily: corev1.IPv6Protocol, + Gateway: "2001:db8::1", + SubnetMask: "ffff:ffff:ffff:ffff::", + }, + } + netInterface.Status.Conditions = []netopv1alpha1.NetworkInterfaceCondition{ + { + Type: netopv1alpha1.NetworkInterfaceReady, + Status: corev1.ConditionTrue, + }, + } + Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) + }) + + results, err = network.CreateAndWaitForNetworkInterfaces( + vmCtx, + ctx.Client, + ctx.VCClient.Client, + ctx.Finder, + nil, + networkSpec) + Expect(err).ToNot(HaveOccurred()) + + Expect(results.Results).To(HaveLen(1)) + result := results.Results[0] + Expect(result.IPConfigs[0].Gateway).To(Equal("172.16.1.1")) + Expect(result.IPConfigs[1].Gateway).To(Equal("2001:db8::2")) + }) + }) + + Context("User Gateways 'None' Clears NetOP Gateways", func() { + BeforeEach(func() { + networkSpec.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName, + Network: &common.PartialObjectRef{ + Name: networkName, + }, + Addresses: []string{"192.168.1.100/24", "2001:db8::100/64"}, + Gateway4: "None", + Gateway6: "None", + }, + } + }) + + It("returns success with gateways cleared", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + + By("simulate successful NetOP reconcile", func() { + netInterface := &netopv1alpha1.NetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) + netInterface.Status.NetworkID = ctx.NetworkRef.Reference().Value + netInterface.Status.IPAssignmentMode = netopv1alpha1.NetworkInterfaceIPAssignmentModeStaticPool + netInterface.Status.IPConfigs = []netopv1alpha1.IPConfig{ + { + IP: "192.168.1.100", + IPFamily: corev1.IPv4Protocol, + Gateway: "192.168.1.1", + SubnetMask: "255.255.255.0", + }, + { + IP: "2001:db8::100", + IPFamily: corev1.IPv6Protocol, + Gateway: "2001:db8::1", + SubnetMask: "ffff:ffff:ffff:ffff::", + }, + } + netInterface.Status.Conditions = []netopv1alpha1.NetworkInterfaceCondition{ + { + Type: netopv1alpha1.NetworkInterfaceReady, + Status: corev1.ConditionTrue, + }, + } + Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) + }) + + results, err = network.CreateAndWaitForNetworkInterfaces( + vmCtx, + ctx.Client, + ctx.VCClient.Client, + ctx.Finder, + nil, + networkSpec) + Expect(err).ToNot(HaveOccurred()) + + Expect(results.Results).To(HaveLen(1)) + result := results.Results[0] + Expect(result.IPConfigs[0].Gateway).To(BeEmpty()) + Expect(result.IPConfigs[1].Gateway).To(BeEmpty()) + }) + }) + + Context("User DHCP4 Overrides NetOP StaticPool", func() { + BeforeEach(func() { + networkSpec.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName, + Network: &common.PartialObjectRef{ + Name: networkName, + }, + Addresses: []string{"2001:db8::100/64"}, + DHCP4: true, + }, + } + }) + + It("returns success with DHCP4 enabled and static IPv6", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + + By("simulate successful NetOP reconcile", func() { + netInterface := &netopv1alpha1.NetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) + netInterface.Status.NetworkID = ctx.NetworkRef.Reference().Value + netInterface.Status.IPAssignmentMode = netopv1alpha1.NetworkInterfaceIPAssignmentModeStaticPool + netInterface.Status.IPConfigs = []netopv1alpha1.IPConfig{ + { + IP: "192.168.1.100", + IPFamily: corev1.IPv4Protocol, + Gateway: "192.168.1.1", + SubnetMask: "255.255.255.0", + }, + { + IP: "2001:db8::100", + IPFamily: corev1.IPv6Protocol, + Gateway: "2001:db8::1", + SubnetMask: "ffff:ffff:ffff:ffff::", + }, + } + netInterface.Status.Conditions = []netopv1alpha1.NetworkInterfaceCondition{ + { + Type: netopv1alpha1.NetworkInterfaceReady, + Status: corev1.ConditionTrue, + }, + } + Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) + }) + + results, err = network.CreateAndWaitForNetworkInterfaces( + vmCtx, + ctx.Client, + ctx.VCClient.Client, + ctx.Finder, + nil, + networkSpec) + Expect(err).ToNot(HaveOccurred()) + + Expect(results.Results).To(HaveLen(1)) + result := results.Results[0] + Expect(result.DHCP4).To(BeTrue()) + Expect(result.DHCP6).To(BeFalse()) + Expect(result.IPConfigs).To(HaveLen(1)) + Expect(result.IPConfigs[0].IPCIDR).To(Equal("2001:db8::100/64")) + }) + }) + + Context("User DHCP6 Overrides NetOP StaticPool", func() { + BeforeEach(func() { + networkSpec.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName, + Network: &common.PartialObjectRef{ + Name: networkName, + }, + Addresses: []string{"192.168.1.100/24"}, + DHCP6: true, + }, + } + }) + + It("returns success with static IPv4 and DHCP6 enabled", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + + By("simulate successful NetOP reconcile", func() { + netInterface := &netopv1alpha1.NetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) + netInterface.Status.NetworkID = ctx.NetworkRef.Reference().Value + netInterface.Status.IPAssignmentMode = netopv1alpha1.NetworkInterfaceIPAssignmentModeStaticPool + netInterface.Status.IPConfigs = []netopv1alpha1.IPConfig{ + { + IP: "192.168.1.100", + IPFamily: corev1.IPv4Protocol, + Gateway: "192.168.1.1", + SubnetMask: "255.255.255.0", + }, + { + IP: "2001:db8::100", + IPFamily: corev1.IPv6Protocol, + Gateway: "2001:db8::1", + SubnetMask: "ffff:ffff:ffff:ffff::", + }, + } + netInterface.Status.Conditions = []netopv1alpha1.NetworkInterfaceCondition{ + { + Type: netopv1alpha1.NetworkInterfaceReady, + Status: corev1.ConditionTrue, + }, + } + Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) + }) + + results, err = network.CreateAndWaitForNetworkInterfaces( + vmCtx, + ctx.Client, + ctx.VCClient.Client, + ctx.Finder, + nil, + networkSpec) + Expect(err).ToNot(HaveOccurred()) + + Expect(results.Results).To(HaveLen(1)) + result := results.Results[0] + Expect(result.DHCP4).To(BeFalse()) + Expect(result.DHCP6).To(BeTrue()) + Expect(result.IPConfigs).To(HaveLen(1)) + Expect(result.IPConfigs[0].IPCIDR).To(Equal("192.168.1.100/24")) + }) + }) + + Context("NetOP Addresses with User Gateways", func() { + BeforeEach(func() { + networkSpec.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName, + Network: &common.PartialObjectRef{ + Name: networkName, + }, + Gateway4: "172.16.1.1", + Gateway6: "2001:db8::2", + }, + } + }) + + It("returns success with user gateways overriding NetOP", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + + By("simulate successful NetOP reconcile", func() { + netInterface := &netopv1alpha1.NetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) + netInterface.Status.NetworkID = ctx.NetworkRef.Reference().Value + netInterface.Status.IPAssignmentMode = netopv1alpha1.NetworkInterfaceIPAssignmentModeStaticPool + netInterface.Status.IPConfigs = []netopv1alpha1.IPConfig{ + { + IP: "192.168.1.100", + IPFamily: corev1.IPv4Protocol, + Gateway: "192.168.1.1", + SubnetMask: "255.255.255.0", + }, + { + IP: "2001:db8::100", + IPFamily: corev1.IPv6Protocol, + Gateway: "2001:db8::1", + SubnetMask: "ffff:ffff:ffff:ffff::", + }, + } + netInterface.Status.Conditions = []netopv1alpha1.NetworkInterfaceCondition{ + { + Type: netopv1alpha1.NetworkInterfaceReady, + Status: corev1.ConditionTrue, + }, + } + Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) + }) + + results, err = network.CreateAndWaitForNetworkInterfaces( + vmCtx, + ctx.Client, + ctx.VCClient.Client, + ctx.Finder, + nil, + networkSpec) + Expect(err).ToNot(HaveOccurred()) + + Expect(results.Results).To(HaveLen(1)) + result := results.Results[0] + Expect(result.IPConfigs[0].Gateway).To(Equal("172.16.1.1")) + Expect(result.IPConfigs[1].Gateway).To(Equal("2001:db8::2")) + }) + }) }) Context("NCP", func() {