diff --git a/test/e2e/kubevirt.go b/test/e2e/kubevirt.go index 44fc9c6e100..fe58f6d297e 100644 --- a/test/e2e/kubevirt.go +++ b/test/e2e/kubevirt.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net" + "net/netip" "os" "os/exec" "strings" @@ -14,6 +15,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/config" "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/util" "github.com/ovn-org/ovn-kubernetes/test/e2e/diagnostics" "github.com/ovn-org/ovn-kubernetes/test/e2e/kubevirt" @@ -71,6 +73,10 @@ func newControllerRuntimeClient() (crclient.Client, error) { if err != nil { return nil, err } + err = corev1.AddToScheme(scheme) + if err != nil { + return nil, err + } return crclient.New(config, crclient.Options{ Scheme: scheme, }) @@ -303,8 +309,8 @@ var _ = Describe("Kubevirt Virtual Machines", func() { checkPodRunningReady = func() func(Gomega, *corev1.Pod) { return func(g Gomega, pod *corev1.Pod) { ok, err := testutils.PodRunningReady(pod) - Expect(err).ToNot(HaveOccurred()) - Expect(ok).To(BeTrue()) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(ok).To(BeTrue()) } } @@ -1420,4 +1426,130 @@ passwd: }), ) }) + Context("with kubevirt VM using layer2 UDPN", func() { + var ( + podName = "virt-launcher-vm1" + cidrIPv4 = "10.128.0.0/24" + cidrIPv6 = "2010:100:200::/60" + primaryUDNNetworkStatus nadapi.NetworkStatus + virtLauncherCommand = func(command string) (string, error) { + stdout, stderr, err := ExecShellInPodWithFullOutput(fr, namespace, podName, command) + if err != nil { + return "", fmt.Errorf("%s: %s: %w", stdout, stderr, err) + } + return stdout, nil + } + primaryUDNValueFor = func(ty, field string) ([]string, error) { + output, err := virtLauncherCommand(fmt.Sprintf(`nmcli -e no -g %s %s show ovn-udn1`, field, ty)) + if err != nil { + return nil, err + } + return strings.Split(output, " | "), nil + } + primaryUDNValueForConnection = func(field string) ([]string, error) { + return primaryUDNValueFor("connection", field) + } + primaryUDNValueForDevice = func(field string) ([]string, error) { + return primaryUDNValueFor("device", field) + } + ) + BeforeEach(func() { + netConfig := newNetworkAttachmentConfig( + networkAttachmentConfigParams{ + namespace: namespace, + name: "net1", + topology: "layer2", + cidr: correctCIDRFamily(cidrIPv4, cidrIPv6), + role: "primary", + mtu: 1300, + }) + By("Creating NetworkAttachmentDefinition") + Expect(crClient.Create(context.Background(), generateNAD(netConfig))).To(Succeed()) + + By("Create virt-launcher pod") + kubevirtPod := kubevirt.GenerateFakeVirtLauncherPod(namespace, "vm1") + Expect(crClient.Create(context.Background(), kubevirtPod)).To(Succeed()) + + By("Wait for virt-launcher pod to be ready and primary UDN network status to pop up") + waitForPodsCondition([]*corev1.Pod{kubevirtPod}, func(g Gomega, pod *corev1.Pod) { + ok, err := testutils.PodRunningReady(pod) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(ok).To(BeTrue()) + + primaryUDNNetworkStatuses, err := podNetworkStatus(pod, func(networkStatus nadapi.NetworkStatus) bool { + return networkStatus.Default + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(primaryUDNNetworkStatuses).To(HaveLen(1)) + primaryUDNNetworkStatus = primaryUDNNetworkStatuses[0] + }) + + By("Wait NetworkManager readiness") + Eventually(func() error { + _, err := virtLauncherCommand("systemctl is-active NetworkManager") + return err + }). + WithTimeout(5 * time.Second). + WithPolling(time.Second). + Should(Succeed()) + + By("Reconfigure primary UDN interface to use dhcp/nd for ipv4 and ipv6") + _, err := virtLauncherCommand(kubevirt.GenerateAddressDiscoveryConfigurationCommand("ovn-udn1")) + Expect(err).ToNot(HaveOccurred()) + + }) + It("should configure IPv4 and IPv6 using DHCP and NDP", func() { + dnsService, err := fr.ClientSet.CoreV1().Services(config.Kubernetes.DNSServiceNamespace). + Get(context.Background(), config.Kubernetes.DNSServiceName, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + + if isIPv4Supported() { + expectedIP, err := matchIPv4StringFamily(primaryUDNNetworkStatus.IPs) + Expect(err).ToNot(HaveOccurred()) + + expectedDNS, err := matchIPv4StringFamily(dnsService.Spec.ClusterIPs) + Expect(err).ToNot(HaveOccurred()) + + _, cidr, err := net.ParseCIDR(cidrIPv4) + Expect(err).ToNot(HaveOccurred()) + expectedGateway := util.GetNodeGatewayIfAddr(cidr).IP.String() + + Eventually(primaryUDNValueForConnection). + WithArguments("DHCP4.OPTION"). + WithTimeout(10 * time.Second). + WithPolling(time.Second). + Should(ContainElements( + "host_name = vm1", + fmt.Sprintf("ip_address = %s", expectedIP), + fmt.Sprintf("domain_name_servers = %s", expectedDNS), + fmt.Sprintf("routers = %s", expectedGateway), + fmt.Sprintf("interface_mtu = 1300"), + )) + Expect(primaryUDNValueForConnection("IP4.ADDRESS")).To(ConsistOf(expectedIP + "/24")) + Expect(primaryUDNValueForConnection("IP4.GATEWAY")).To(ConsistOf(expectedGateway)) + Expect(primaryUDNValueForConnection("IP4.DNS")).To(ConsistOf(expectedDNS)) + Expect(primaryUDNValueForDevice("GENERAL.MTU")).To(ConsistOf("1300")) + } + + if isIPv6Supported() { + expectedIP, err := matchIPv6StringFamily(primaryUDNNetworkStatus.IPs) + Expect(err).ToNot(HaveOccurred()) + Eventually(primaryUDNValueFor). + WithArguments("connection", "DHCP6.OPTION"). + WithTimeout(10 * time.Second). + WithPolling(time.Second). + Should(ContainElements( + "fqdn_fqdn = vm1", + fmt.Sprintf("ip6_address = %s", expectedIP), + )) + Expect(primaryUDNValueForConnection("IP6.ADDRESS")).To(SatisfyAll(HaveLen(2), ContainElements(expectedIP+"/128"))) + Expect(primaryUDNValueForConnection("IP6.GATEWAY")).To(ConsistOf(WithTransform(func(ipv6 string) bool { + return netip.MustParseAddr(ipv6).IsLinkLocalUnicast() + }, BeTrue()))) + Expect(primaryUDNValueForConnection("IP6.ROUTE")).To(ContainElement(ContainSubstring(fmt.Sprintf("dst = %s", cidrIPv6)))) + Expect(primaryUDNValueForDevice("GENERAL.MTU")).To(ConsistOf("1300")) + } + + }) + }) }) diff --git a/test/e2e/kubevirt/net.go b/test/e2e/kubevirt/net.go new file mode 100644 index 00000000000..f8c55cbf451 --- /dev/null +++ b/test/e2e/kubevirt/net.go @@ -0,0 +1,11 @@ +package kubevirt + +import ( + "fmt" +) + +func GenerateAddressDiscoveryConfigurationCommand(iface string) string { + return fmt.Sprintf(` +nmcli c mod %[1]s ipv4.addresses "" ipv6.addresses "" ipv4.gateway "" ipv6.gateway "" ipv6.method auto ipv4.method auto ipv6.addr-gen-mode eui64 +nmcli d reapply %[1]s`, iface) +} diff --git a/test/e2e/kubevirt/pod.go b/test/e2e/kubevirt/pod.go new file mode 100644 index 00000000000..f70175c9064 --- /dev/null +++ b/test/e2e/kubevirt/pod.go @@ -0,0 +1,33 @@ +package kubevirt + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + kubevirtv1 "kubevirt.io/api/core/v1" +) + +func GenerateFakeVirtLauncherPod(namespace, vmName string) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "virt-launcher-" + vmName, + Namespace: namespace, + Labels: map[string]string{ + kubevirtv1.VirtualMachineNameLabel: vmName, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "compute", + Image: "quay.io/nmstate/c10s-nmstate-dev:latest", + SecurityContext: &corev1.SecurityContext{ + Privileged: ptr.To(true), + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + }, + }, + }}, + }, + } +} diff --git a/test/e2e/multihoming_utils.go b/test/e2e/multihoming_utils.go index 874fcc4eb43..19bd1c6ea3d 100644 --- a/test/e2e/multihoming_utils.go +++ b/test/e2e/multihoming_utils.go @@ -61,6 +61,7 @@ type networkAttachmentConfigParams struct { vlanID int allowPersistentIPs bool role string + mtu int } type networkAttachmentConfig struct { @@ -83,6 +84,9 @@ func uniqueNadName(originalNetName string) string { } func generateNAD(config networkAttachmentConfig) *nadapi.NetworkAttachmentDefinition { + if config.mtu == 0 { + config.mtu = 1300 + } nadSpec := fmt.Sprintf( ` { @@ -92,7 +96,7 @@ func generateNAD(config networkAttachmentConfig) *nadapi.NetworkAttachmentDefini "topology":%q, "subnets": %q, "excludeSubnets": %q, - "mtu": 1300, + "mtu": %d, "netAttachDefName": %q, "vlanID": %d, "allowPersistentIPs": %t, @@ -103,6 +107,7 @@ func generateNAD(config networkAttachmentConfig) *nadapi.NetworkAttachmentDefini config.topology, config.cidr, strings.Join(config.excludeCIDRs, ","), + config.mtu, namespacedName(config.namespace, config.name), config.vlanID, config.allowPersistentIPs, diff --git a/test/e2e/util.go b/test/e2e/util.go index 27f645c6278..d788540fd68 100644 --- a/test/e2e/util.go +++ b/test/e2e/util.go @@ -14,6 +14,7 @@ import ( "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" + "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/util" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -1247,3 +1248,11 @@ func isKernelModuleLoaded(nodeName, kernelModuleName string) bool { } return false } + +func matchIPv4StringFamily(ipStrings []string) (string, error) { + return util.MatchIPStringFamily(false /*ipv4*/, ipStrings) +} + +func matchIPv6StringFamily(ipStrings []string) (string, error) { + return util.MatchIPStringFamily(true /*ipv6*/, ipStrings) +}