From a963f964ca1cffdf620fddc7b9599144ad8bcee3 Mon Sep 17 00:00:00 2001 From: Ram Lavi Date: Tue, 22 Jul 2025 17:14:26 +0300 Subject: [PATCH 1/4] networking: Add primaryUDN VM Addr collision test w/ preconfigured addr Validates that KubeVirt VMs with preconfigured MAC and IP addresses maintain those addresses correctly before and after a vmi with duplicate IP/MAC request is made, and that the vmi with the duplicate address get the appropriate address conflict error event. Co-authored-by: Miguel Duarte Barroso Signed-off-by: Ram Lavi (cherry picked from commit 6dd2a935880fcfbedac15f6e3fbdd500c7ffdf80) --- test/extended/networking/kubevirt/client.go | 60 +++++++++++++++++++++ test/extended/networking/livemigration.go | 48 +++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/test/extended/networking/kubevirt/client.go b/test/extended/networking/kubevirt/client.go index 77b813b68284..7c3bb548fdaa 100644 --- a/test/extended/networking/kubevirt/client.go +++ b/test/extended/networking/kubevirt/client.go @@ -16,6 +16,8 @@ import ( "strings" "time" + "sigs.k8s.io/yaml" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl" @@ -93,6 +95,64 @@ func (c *Client) GetJSONPath(resource, name, jsonPath string) (string, error) { } return strings.TrimSuffix(strings.TrimPrefix(output, `"`), `"`), nil } + +func (c *Client) GetPodsByLabel(labelKey, labelValue string) ([]string, error) { + output, err := c.oc.AsAdmin().Run("get").Args("pods", "-n", c.oc.Namespace(), "-l", fmt.Sprintf("%s=%s", labelKey, labelValue), "-o", "name").Output() + if err != nil { + return nil, err + } + if output == "" { + return []string{}, nil + } + + lines := strings.Split(strings.TrimSpace(output), "\n") + podNames := make([]string, 0, len(lines)) + for _, line := range lines { + if line != "" { + podName := strings.TrimPrefix(line, "pod/") + podNames = append(podNames, podName) + } + } + return podNames, nil +} + +func (c *Client) GetEventsForPod(podName string) ([]string, error) { + output, err := c.oc.AsAdmin().Run("get").Args("events", "-n", c.oc.Namespace(), "--field-selector", fmt.Sprintf("involvedObject.name=%s,involvedObject.kind=Pod", podName), "-o", "custom-columns=MESSAGE:.message", "--no-headers").Output() + if err != nil { + return nil, err + } + if output == "" { + return []string{}, nil + } + lines := strings.Split(strings.TrimSpace(output), "\n") + messages := make([]string, 0, len(lines)) + for _, line := range lines { + if line != "" { + messages = append(messages, line) + } + } + return messages, nil +} + +func (c *Client) CreateVMIFromSpec(vmNamespace, vmName string, vmiSpec map[string]interface{}) error { + newVMI := map[string]interface{}{ + "apiVersion": "kubevirt.io/v1", + "kind": "VirtualMachineInstance", + "metadata": map[string]interface{}{ + "name": vmName, + "namespace": vmNamespace, + }, + "spec": vmiSpec, + } + + newVMIYAML, err := yaml.Marshal(newVMI) + if err != nil { + return err + } + + return c.Apply(string(newVMIYAML)) +} + func ensureVirtctl(oc *exutil.CLI, dir string) (string, error) { filepath := filepath.Join(dir, "virtctl") _, err := os.Stat(filepath) diff --git a/test/extended/networking/livemigration.go b/test/extended/networking/livemigration.go index 3ff4e8d22335..1176c72e60ad 100644 --- a/test/extended/networking/livemigration.go +++ b/test/extended/networking/livemigration.go @@ -312,6 +312,20 @@ var _ = Describe("[sig-network][OCPFeatureGate:PersistentIPsForVirtualization][F preconfiguredMAC: "02:0A:0B:0C:0D:51", }, ), + Entry( + "[OCPFeatureGate:PreconfiguredUDNAddresses] when the VM with preconfigured IP address is created when the address is already taken", + networkAttachmentConfigParams{ + name: nadName, + topology: "layer2", + role: "primary", + allowPersistentIPs: true, + }, + kubevirt.FedoraVMWithPreconfiguredPrimaryUDNAttachment, + duplicateVM, + workloadNetworkConfig{ + preconfiguredIPs: []string{"203.203.0.100", "2014:100:200::100"}, + }, + ), ) }, Entry("NetworkAttachmentDefinitions", func(c networkAttachmentConfigParams) networkAttachmentConfig { @@ -537,6 +551,40 @@ func verifyVMMAC(virtClient *kubevirt.Client, vmName, expectedMAC string) { Should(Equal(expectedMAC)) } +func duplicateVM(cli *kubevirt.Client, vmNamespace, vmName string) { + GinkgoHelper() + duplicateVMName := vmName + "-duplicate" + By(fmt.Sprintf("Duplicating VM %s/%s to %s/%s", vmNamespace, vmName, vmNamespace, duplicateVMName)) + + vmiSpecJSON, err := cli.GetJSONPath("vmi", vmName, "{.spec}") + Expect(err).NotTo(HaveOccurred()) + var vmiSpec map[string]interface{} + Expect(json.Unmarshal([]byte(vmiSpecJSON), &vmiSpec)).To(Succeed()) + + Expect(cli.CreateVMIFromSpec(vmNamespace, duplicateVMName, vmiSpec)).To(Succeed()) + waitForVMPodEventWithMessage(cli, vmNamespace, duplicateVMName, "IP is already allocated", 2*time.Minute) +} + +func waitForVMPodEventWithMessage(vmClient *kubevirt.Client, vmNamespace, vmName, expectedEventMessage string, timeout time.Duration) { + GinkgoHelper() + By(fmt.Sprintf("Waiting for event containing %q on VM %s/%s virt-launcher pod", expectedEventMessage, vmNamespace, vmName)) + + Eventually(func(g Gomega) []string { + const vmLabelKey = "vm.kubevirt.io/name" + podNames, err := vmClient.GetPodsByLabel(vmLabelKey, vmName) + g.Expect(err).NotTo(HaveOccurred(), "Failed to get pods by label %s=%s", vmLabelKey, vmName) + g.Expect(podNames).To(HaveLen(1), "Expected exactly one virt-launcher pod for VM %s/%s, but found %d pods: %v", vmNamespace, vmName, len(podNames), podNames) + + virtLauncherPodName := podNames[0] + eventMessages, err := vmClient.GetEventsForPod(virtLauncherPodName) + g.Expect(err).NotTo(HaveOccurred(), "Failed to get events for pod %s", virtLauncherPodName) + + return eventMessages + }).WithPolling(time.Second).WithTimeout(timeout).Should( + ContainElement(ContainSubstring(expectedEventMessage)), + fmt.Sprintf("Expected to find an event containing %q", expectedEventMessage)) +} + func waitForPodsCondition(fr *framework.Framework, pods []*corev1.Pod, conditionFn func(g Gomega, pod *corev1.Pod)) { for _, pod := range pods { Eventually(func(g Gomega) { From 0e998a6f5ea0e160d55e6d9155b4505f3d85aff8 Mon Sep 17 00:00:00 2001 From: Miguel Duarte Barroso Date: Thu, 14 Aug 2025 16:41:27 +0100 Subject: [PATCH 2/4] networking, virt: carry over the requested IPs from the original VM The requested IPs for the primary UDN attachment are not in the VMI spec, but in an annotation in the VMI. Hence, we need to fetch that particular annotation, and set it in the duplicate VMI. This was implemented using the builder pattern, since I suspect in the future we will need to further customize the VMI spec / metadata; this will make it simpler to extend the framework in the future. Signed-off-by: Miguel Duarte Barroso (cherry picked from commit 13fdf10ed4e78c2c183e011be5bacf3f46112016) --- test/extended/networking/kubevirt/client.go | 19 ++++++++++++++++++- test/extended/networking/livemigration.go | 21 ++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/test/extended/networking/kubevirt/client.go b/test/extended/networking/kubevirt/client.go index 7c3bb548fdaa..61fa2792fef8 100644 --- a/test/extended/networking/kubevirt/client.go +++ b/test/extended/networking/kubevirt/client.go @@ -134,7 +134,9 @@ func (c *Client) GetEventsForPod(podName string) ([]string, error) { return messages, nil } -func (c *Client) CreateVMIFromSpec(vmNamespace, vmName string, vmiSpec map[string]interface{}) error { +type Option func(map[string]interface{}) + +func (c *Client) CreateVMIFromSpec(vmNamespace, vmName string, vmiSpec map[string]interface{}, opts ...Option) error { newVMI := map[string]interface{}{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", @@ -145,6 +147,10 @@ func (c *Client) CreateVMIFromSpec(vmNamespace, vmName string, vmiSpec map[strin "spec": vmiSpec, } + for _, opt := range opts { + opt(newVMI) + } + newVMIYAML, err := yaml.Marshal(newVMI) if err != nil { return err @@ -153,6 +159,17 @@ func (c *Client) CreateVMIFromSpec(vmNamespace, vmName string, vmiSpec map[strin return c.Apply(string(newVMIYAML)) } +func WithAnnotations(annotations map[string]string) Option { + return func(cr map[string]interface{}) { + metadata, hasMetadata := cr["metadata"].(map[string]interface{}) + if !hasMetadata { + metadata = make(map[string]interface{}) + cr["metadata"] = metadata + } + metadata["annotations"] = annotations + } +} + func ensureVirtctl(oc *exutil.CLI, dir string) (string, error) { filepath := filepath.Join(dir, "virtctl") _, err := os.Stat(filepath) diff --git a/test/extended/networking/livemigration.go b/test/extended/networking/livemigration.go index 1176c72e60ad..e4c09932fcc1 100644 --- a/test/extended/networking/livemigration.go +++ b/test/extended/networking/livemigration.go @@ -30,6 +30,8 @@ import ( "github.com/openshift/origin/test/extended/util/image" ) +const kvIPRequestsAnnot = "network.kubevirt.io/addresses" + var _ = Describe("[sig-network][OCPFeatureGate:PersistentIPsForVirtualization][Feature:Layer2LiveMigration] Kubevirt Virtual Machines", func() { // disable automatic namespace creation, we need to add the required UDN label oc := exutil.NewCLIWithoutNamespace("network-segmentation-e2e") @@ -561,7 +563,20 @@ func duplicateVM(cli *kubevirt.Client, vmNamespace, vmName string) { var vmiSpec map[string]interface{} Expect(json.Unmarshal([]byte(vmiSpecJSON), &vmiSpec)).To(Succeed()) - Expect(cli.CreateVMIFromSpec(vmNamespace, duplicateVMName, vmiSpec)).To(Succeed()) + originalVMIRawAnnotations, err := cli.GetJSONPath("vmi", vmName, "{.metadata.annotations}") + Expect(err).NotTo(HaveOccurred()) + + originalVMIAnnotations := map[string]string{} + Expect(json.Unmarshal([]byte(originalVMIRawAnnotations), &originalVMIAnnotations)).To(Succeed()) + + var vmiCreationOptions []kubevirt.Option + if requestedIPs, hasIPRequests := originalVMIAnnotations[kvIPRequestsAnnot]; hasIPRequests { + vmiCreationOptions = append( + vmiCreationOptions, + kubevirt.WithAnnotations(ipRequests(requestedIPs)), + ) + } + Expect(cli.CreateVMIFromSpec(vmNamespace, duplicateVMName, vmiSpec, vmiCreationOptions...)).To(Succeed()) waitForVMPodEventWithMessage(cli, vmNamespace, duplicateVMName, "IP is already allocated", 2*time.Minute) } @@ -792,3 +807,7 @@ func formatAddressesAnnotation(preconfiguredIPs []string) (string, error) { return string(staticIPs), nil } + +func ipRequests(ips string) map[string]string { + return map[string]string{kvIPRequestsAnnot: ips} +} From cc0da146652e1e2e36000e39eaa2540550a0ab6d Mon Sep 17 00:00:00 2001 From: Miguel Duarte Barroso Date: Wed, 20 Aug 2025 13:26:16 +0100 Subject: [PATCH 3/4] networking, virt: generalize conflict detection This way we can ensure appropriate cases are caught in a generic way. With it, we can safely expect to find IP conflicts when there are duplicate IPs in the network. In future commits, we will be able to catch MAC conflicts when there are duplicate MACs in the network Co-authored-by: Ram Lavi Signed-off-by: Miguel Duarte Barroso (cherry picked from commit fdb0584ecf22c2a5c3cf3534f1398e89f3f8cb5b) --- test/extended/networking/livemigration.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/extended/networking/livemigration.go b/test/extended/networking/livemigration.go index e4c09932fcc1..79c3d58e199c 100644 --- a/test/extended/networking/livemigration.go +++ b/test/extended/networking/livemigration.go @@ -570,14 +570,26 @@ func duplicateVM(cli *kubevirt.Client, vmNamespace, vmName string) { Expect(json.Unmarshal([]byte(originalVMIRawAnnotations), &originalVMIAnnotations)).To(Succeed()) var vmiCreationOptions []kubevirt.Option + var vmiExpectations []func() if requestedIPs, hasIPRequests := originalVMIAnnotations[kvIPRequestsAnnot]; hasIPRequests { vmiCreationOptions = append( vmiCreationOptions, kubevirt.WithAnnotations(ipRequests(requestedIPs)), ) + vmiExpectations = append(vmiExpectations, func() { + waitForVMPodEventWithMessage( + cli, + vmNamespace, + duplicateVMName, + "IP is already allocated", + 2*time.Minute, + ) + }) } Expect(cli.CreateVMIFromSpec(vmNamespace, duplicateVMName, vmiSpec, vmiCreationOptions...)).To(Succeed()) - waitForVMPodEventWithMessage(cli, vmNamespace, duplicateVMName, "IP is already allocated", 2*time.Minute) + for _, expectation := range vmiExpectations { + expectation() + } } func waitForVMPodEventWithMessage(vmClient *kubevirt.Client, vmNamespace, vmName, expectedEventMessage string, timeout time.Duration) { From 49ac8bc85fbe2b05142ce96746b15c35e807983d Mon Sep 17 00:00:00 2001 From: Or Mergi Date: Mon, 29 Sep 2025 17:43:47 +0300 Subject: [PATCH 4/4] ovn-k, virt, preconfigured-udn-addrs: Test MAC conflict detection Verify KubeVirt VMs with preconfigured MAC address retain a requested MAC address before and after another VM is created requesting the same MAC address. And verify the other VM requesting the same MAC address get the appropriate address conflict error event. Signed-off-by: Or Mergi (cherry picked from commit 0c58f2a6a4feb576d44e5df515cf3213694c3c80) --- test/extended/networking/livemigration.go | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/extended/networking/livemigration.go b/test/extended/networking/livemigration.go index 79c3d58e199c..4fb89c29f14b 100644 --- a/test/extended/networking/livemigration.go +++ b/test/extended/networking/livemigration.go @@ -328,6 +328,20 @@ var _ = Describe("[sig-network][OCPFeatureGate:PersistentIPsForVirtualization][F preconfiguredIPs: []string{"203.203.0.100", "2014:100:200::100"}, }, ), + Entry( + "[OCPFeatureGate:PreconfiguredUDNAddresses] when the VM with preconfigured MAC address is created when the address is already taken", + networkAttachmentConfigParams{ + name: nadName, + topology: "layer2", + role: "primary", + allowPersistentIPs: true, + }, + kubevirt.FedoraVMWithPreconfiguredPrimaryUDNAttachment, + duplicateVM, + workloadNetworkConfig{ + preconfiguredMAC: "02:00:00:22:22:22", + }, + ), ) }, Entry("NetworkAttachmentDefinitions", func(c networkAttachmentConfigParams) networkAttachmentConfig { @@ -586,6 +600,21 @@ func duplicateVM(cli *kubevirt.Client, vmNamespace, vmName string) { ) }) } + + mac, err := cli.GetJSONPath("vmi", vmName, "{.spec.domain.devices.interfaces[0].macAddress}") + Expect(err).NotTo(HaveOccurred()) + if len(mac) > 0 { + vmiExpectations = append(vmiExpectations, func() { + waitForVMPodEventWithMessage( + cli, + vmNamespace, + duplicateVMName, + "MAC address already in use", + 2*time.Minute, + ) + }) + } + Expect(cli.CreateVMIFromSpec(vmNamespace, duplicateVMName, vmiSpec, vmiCreationOptions...)).To(Succeed()) for _, expectation := range vmiExpectations { expectation()