diff --git a/test/extended/networking/kubevirt/client.go b/test/extended/networking/kubevirt/client.go index 77b813b68284..61fa2792fef8 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,81 @@ 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 +} + +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", + "metadata": map[string]interface{}{ + "name": vmName, + "namespace": vmNamespace, + }, + "spec": vmiSpec, + } + + for _, opt := range opts { + opt(newVMI) + } + + newVMIYAML, err := yaml.Marshal(newVMI) + if err != nil { + return err + } + + 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 3ff4e8d22335..4fb89c29f14b 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") @@ -312,6 +314,34 @@ 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( + "[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 { @@ -537,6 +567,80 @@ 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()) + + 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 + 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, + ) + }) + } + + 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() + } +} + +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) { @@ -744,3 +848,7 @@ func formatAddressesAnnotation(preconfiguredIPs []string) (string, error) { return string(staticIPs), nil } + +func ipRequests(ips string) map[string]string { + return map[string]string{kvIPRequestsAnnot: ips} +}