From 383a4c518ff0bbb4ff49c3379af3f1d1f521f6a3 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Mon, 9 Feb 2026 09:30:29 +0200 Subject: [PATCH 1/7] feat(e2e): add vm without main network test Signed-off-by: Daniil Antoshin --- test/e2e/internal/util/sdn.go | 12 + test/e2e/legacy/vm_vpc.go | 204 ------------ test/e2e/vm/additional_network_interfaces.go | 331 +++++++++++++++++++ 3 files changed, 343 insertions(+), 204 deletions(-) delete mode 100644 test/e2e/legacy/vm_vpc.go create mode 100644 test/e2e/vm/additional_network_interfaces.go diff --git a/test/e2e/internal/util/sdn.go b/test/e2e/internal/util/sdn.go index 74dbd39f24..f411cc8014 100644 --- a/test/e2e/internal/util/sdn.go +++ b/test/e2e/internal/util/sdn.go @@ -58,6 +58,18 @@ func IsSdnModuleEnabled(f *framework.Framework) bool { return enabled != nil && *enabled } +// IsSdnModuleEnabledOrError returns whether SDN module is enabled, or an error if config cannot be read. +// Used by legacy tests that do not have a framework instance. +func IsSdnModuleEnabledOrError() (bool, error) { + f := framework.NewFramework("") + sdnModule, err := f.GetModuleConfig("sdn") + if err != nil { + return false, err + } + enabled := sdnModule.Spec.Enabled + return enabled != nil && *enabled, nil +} + func IsClusterNetworkExists(f *framework.Framework) bool { GinkgoHelper() diff --git a/test/e2e/legacy/vm_vpc.go b/test/e2e/legacy/vm_vpc.go deleted file mode 100644 index b89a182ce4..0000000000 --- a/test/e2e/legacy/vm_vpc.go +++ /dev/null @@ -1,204 +0,0 @@ -/* -Copyright 2025 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package legacy - -import ( - "fmt" - "strings" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" - "github.com/deckhouse/virtualization/test/e2e/internal/config" - "github.com/deckhouse/virtualization/test/e2e/internal/framework" - kc "github.com/deckhouse/virtualization/test/e2e/internal/kubectl" -) - -func WaitForVMNetworkReady(opts kc.WaitOptions) { - GinkgoHelper() - WaitConditionIsTrueByLabel(kc.ResourceVM, vmcondition.TypeNetworkReady.String(), opts) -} - -func WaitForVMRunningPhase(opts kc.WaitOptions) { - GinkgoHelper() - WaitPhaseByLabel(kc.ResourceVM, PhaseRunning, opts) -} - -var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", Ordered, func() { - testCaseLabel := map[string]string{"testcase": "vm-vpc"} - var ns string - - BeforeAll(func() { - sdnEnabled, err := isSdnModuleEnabled() - if err != nil || !sdnEnabled { - Skip("Module SDN is disabled. Skipping all tests for module SDN.") - } - - kustomization := fmt.Sprintf("%s/%s", conf.TestData.VMVpc, "kustomization.yaml") - ns, err = kustomize.GetNamespace(kustomization) - Expect(err).NotTo(HaveOccurred(), "%w", err) - - CreateNamespace(ns) - }) - - AfterAll(func() { - if CurrentSpecReport().Failed() { - SaveTestCaseDump(testCaseLabel, CurrentSpecReport().LeafNodeText, ns) - } - }) - - Context("When resources are applied", func() { - It("result should be succeeded", func() { - res := kubectl.Apply(kc.ApplyOptions{ - Filename: []string{conf.TestData.VMVpc}, - FilenameOption: kc.Kustomize, - }) - Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) - }) - }) - - Context("When virtual machines are applied", func() { - It("checks VMs phases", func() { - By("Virtual machine should be running") - WaitForVMRunningPhase(kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - It("checks network availability", func() { - By("Network condition should be true") - WaitForVMNetworkReady(kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - - CheckVMConnectivityToTargetIPs(ns, testCaseLabel) - }) - }) - - Context("When virtual machine agents and network are ready", func() { - It("starts migrations", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: testCaseLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) - - vms := strings.Split(res.StdOut(), " ") - MigrateVirtualMachines(testCaseLabel, ns, vms...) - }) - }) - - Context("When VMs migrations are applied", func() { - It("checks VMs and VMOPs phases", func() { - By(fmt.Sprintf("VMOPs should be in %s phases", v1alpha2.VMOPPhaseCompleted)) - WaitPhaseByLabel(kc.ResourceVMOP, string(v1alpha2.VMOPPhaseCompleted), kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - By("Virtual machines should be migrated") - WaitByLabel(kc.ResourceVM, kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - For: "'jsonpath={.status.migrationState.result}=Succeeded'", - }) - }) - - It("checks VMs external connection after migrations", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: testCaseLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) - - vms := strings.Split(res.StdOut(), " ") - Expect(vms).NotTo(BeEmpty()) - - // There is a known issue with the Cilium agent check. - CheckCiliumAgents(kubectl, ns, vms...) - CheckExternalConnection(externalHost, httpStatusOk, ns, vms...) - }) - - It("checks network availability after migrations", func() { - By("Network condition should be true") - WaitForVMNetworkReady(kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - - CheckVMConnectivityToTargetIPs(ns, testCaseLabel) - }) - }) - - Context("When test is completed", func() { - It("deletes test case resources", func() { - resourcesToDelete := ResourcesToDelete{ - AdditionalResources: []AdditionalResource{ - { - Resource: kc.ResourceVMOP, - Labels: testCaseLabel, - }, - }, - } - - if config.IsCleanUpNeeded() { - resourcesToDelete.KustomizationDir = conf.TestData.VMVpc - } - - DeleteTestCaseResources(ns, resourcesToDelete) - }) - }) -}) - -func isSdnModuleEnabled() (bool, error) { - sdnModule, err := framework.NewFramework("").GetModuleConfig("sdn") - if err != nil { - return false, err - } - enabled := sdnModule.Spec.Enabled - - return enabled != nil && *enabled, nil -} - -func CheckVMConnectivityToTargetIPs(ns string, testCaseLabel map[string]string) { - var vmList v1alpha2.VirtualMachineList - err := GetObjects(kc.ResourceVM, &vmList, kc.GetOptions{ - Labels: testCaseLabel, - Namespace: ns, - }) - Expect(err).ShouldNot(HaveOccurred()) - - for _, vm := range vmList.Items { - switch { - case strings.Contains(vm.Name, "foo"): - By(fmt.Sprintf("VM %q should have connectivity to 192.168.1.10 (target: vm-bar)", vm.Name)) - CheckResultSSHCommand(ns, vm.Name, `ping -c 2 -W 2 -w 5 -q 192.168.1.10 2>&1 | grep -o "[0-9]\+%\s*packet loss"`, "0% packet loss") - case strings.Contains(vm.Name, "bar"): - By(fmt.Sprintf("VM %q should have connectivity to 192.168.1.11 (target: vm-foo)", vm.Name)) - CheckResultSSHCommand(ns, vm.Name, `ping -c 2 -W 2 -w 5 -q 192.168.1.11 2>&1 | grep -o "[0-9]\+%\s*packet loss"`, "0% packet loss") - } - } -} diff --git a/test/e2e/vm/additional_network_interfaces.go b/test/e2e/vm/additional_network_interfaces.go new file mode 100644 index 0000000000..31709d4f1e --- /dev/null +++ b/test/e2e/vm/additional_network_interfaces.go @@ -0,0 +1,331 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vm + +import ( + "context" + "fmt" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + + vdbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vd" + vmbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vm" + vmopbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vmop" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" + "github.com/deckhouse/virtualization/test/e2e/internal/framework" + "github.com/deckhouse/virtualization/test/e2e/internal/network" + "github.com/deckhouse/virtualization/test/e2e/internal/object" + "github.com/deckhouse/virtualization/test/e2e/internal/util" +) + +const ( + vmFooAdditionalIP = "192.168.1.10" + vmBarAdditionalIP = "192.168.1.11" +) + +type NetworkConfig struct { + Name string + HasMainNetwork bool + CheckExternalConnectivity bool +} + +var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", func() { + var ( + f *framework.Framework + t *VMAdditionalNetworkTest + ) + + BeforeEach(func() { + f = framework.NewFramework("vm-additional-network") + DeferCleanup(f.After) + f.Before() + + if !util.IsSdnModuleEnabled(f) { + Skip("SDN module is disabled. Skipping tests for additional network interfaces.") + } + + Expect(util.IsClusterNetworkExists(f)).To(BeTrue(), + fmt.Sprintf("Cluster network does not exist. Apply it first: %s", util.ClusterNetworkCreateCommand)) + }) + + DescribeTable("checks VM additional network connectivity and migration", + func(cfg NetworkConfig) { + t = NewVMAdditionalNetworkTest(f, cfg) + + By("Environment preparation", func() { + t.GenerateEnvironmentResources() + err := f.CreateWithDeferredDeletion(context.Background(), t.VDFoo, t.VDBar, t.VMFoo, t.VMBar) + Expect(err).NotTo(HaveOccurred()) + + util.UntilObjectPhase(string(v1alpha2.MachineRunning), framework.LongTimeout, t.VMFoo, t.VMBar) + util.UntilVMAgentReady(crclient.ObjectKeyFromObject(t.VMFoo), framework.LongTimeout) + util.UntilVMAgentReady(crclient.ObjectKeyFromObject(t.VMBar), framework.LongTimeout) + util.UntilConditionStatus(vmcondition.TypeNetworkReady.String(), string(metav1.ConditionTrue), framework.LongTimeout, t.VMFoo, t.VMBar) + + t.CheckCloudInitAdditionalNetworkCompleted(framework.LongTimeout) + }) + + By("Check connectivity between VMs via additional network", func() { + t.CheckVMConnectivityBetweenVMs() + }) + + By("Trigger migrations", func() { + util.MigrateVirtualMachine(f, t.VMFoo, vmopbuilder.WithGenerateName("vmop-migrate-foo-")) + util.MigrateVirtualMachine(f, t.VMBar, vmopbuilder.WithGenerateName("vmop-migrate-bar-")) + }) + + By("Wait for migrations to complete", func() { + util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(t.VMFoo), framework.LongTimeout) + util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(t.VMBar), framework.LongTimeout) + }) + + By("Check Cilium agents after migration", func() { + err := network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), t.VMFoo.Name, f.Namespace().Name) + Expect(err).NotTo(HaveOccurred(), "Cilium agents check should succeed for VM %s", t.VMFoo.Name) + err = network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), t.VMBar.Name, f.Namespace().Name) + Expect(err).NotTo(HaveOccurred(), "Cilium agents check should succeed for VM %s", t.VMBar.Name) + }) + + if cfg.CheckExternalConnectivity { + By("Check external connectivity after migration", func() { + network.CheckExternalConnectivity(f, t.VMFoo.Name, network.ExternalHost, network.HTTPStatusOk) + network.CheckExternalConnectivity(f, t.VMBar.Name, network.ExternalHost, network.HTTPStatusOk) + }) + } + + By("Check network condition after migration", func() { + util.UntilConditionStatus(vmcondition.TypeNetworkReady.String(), string(metav1.ConditionTrue), framework.LongTimeout, t.VMFoo, t.VMBar) + }) + + By("Check connectivity between VMs via additional network after migration", func() { + t.CheckVMConnectivityBetweenVMs() + }) + }, + Entry("with Main and ClusterNetwork", NetworkConfig{Name: "with Main and ClusterNetwork", HasMainNetwork: true, CheckExternalConnectivity: true}), + Entry("only ClusterNetwork", NetworkConfig{Name: "only ClusterNetwork", HasMainNetwork: false, CheckExternalConnectivity: false}), + ) +}) + +type VMAdditionalNetworkTest struct { + Framework *framework.Framework + Config NetworkConfig + + VDFoo *v1alpha2.VirtualDisk + VDBar *v1alpha2.VirtualDisk + VMFoo *v1alpha2.VirtualMachine + VMBar *v1alpha2.VirtualMachine +} + +func NewVMAdditionalNetworkTest(f *framework.Framework, cfg NetworkConfig) *VMAdditionalNetworkTest { + return &VMAdditionalNetworkTest{Framework: f, Config: cfg} +} + +func (t *VMAdditionalNetworkTest) GenerateEnvironmentResources() { + t.VDFoo = vdbuilder.New( + vdbuilder.WithName("vd-foo-root"), + vdbuilder.WithNamespace(t.Framework.Namespace().Name), + vdbuilder.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ + URL: object.ImageURLUbuntu, + }), + ) + + t.VDBar = vdbuilder.New( + vdbuilder.WithName("vd-bar-root"), + vdbuilder.WithNamespace(t.Framework.Namespace().Name), + vdbuilder.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ + URL: object.ImageURLUbuntu, + }), + ) + + t.VMFoo = t.buildVM("vm-foo", t.VDFoo.Name, t.getCloudInitFoo()) + t.VMBar = t.buildVM("vm-bar", t.VDBar.Name, t.getCloudInitBar()) +} + +func (t *VMAdditionalNetworkTest) buildVM(name, vdName, cloudInit string) *v1alpha2.VirtualMachine { + opts := []vmbuilder.Option{ + vmbuilder.WithName(name), + vmbuilder.WithNamespace(t.Framework.Namespace().Name), + vmbuilder.WithCPU(1, ptr.To("50%")), + vmbuilder.WithMemory(resource.MustParse("256Mi")), + vmbuilder.WithLiveMigrationPolicy(v1alpha2.AlwaysSafeMigrationPolicy), + vmbuilder.WithVirtualMachineClass(object.DefaultVMClass), + vmbuilder.WithProvisioningUserData(cloudInit), + vmbuilder.WithBlockDeviceRefs( + v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.DiskDevice, + Name: vdName, + }, + ), + } + if t.Config.HasMainNetwork { + opts = append(opts, + vmbuilder.WithNetwork(v1alpha2.NetworksSpec{Type: v1alpha2.NetworksTypeMain}), + vmbuilder.WithNetwork(v1alpha2.NetworksSpec{ + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: util.ClusterNetworkName, + }), + ) + } else { + opts = append(opts, vmbuilder.WithNetwork(v1alpha2.NetworksSpec{ + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: util.ClusterNetworkName, + })) + } + return vmbuilder.New(opts...) +} + +func (t *VMAdditionalNetworkTest) getCloudInitFoo() string { + return t.getCloudInitWithAdditionalIP(vmFooAdditionalIP) +} + +func (t *VMAdditionalNetworkTest) getCloudInitBar() string { + return t.getCloudInitWithAdditionalIP(vmBarAdditionalIP) +} + +// getCloudInitWithAdditionalIP returns cloud-init. For "Main+ClusterNetwork" it configures the second +// interface with the given static IP. For "only ClusterNetwork" it only installs qemu-guest-agent; +// the single interface gets its IP from SDN (status.IPAddress). +func (t *VMAdditionalNetworkTest) getCloudInitWithAdditionalIP(additionalIP string) string { + base := `#cloud-config +package_update: true +packages: + - qemu-guest-agent +users: + - name: cloud + passwd: $6$rounds=4096$vln/.aPHBOI7BMYR$bBMkqQvuGs5Gyd/1H5DP4m9HjQSy.kgrxpaGEHwkX7KEFV8BS.HZWPitAtZ2Vd8ZqIZRqmlykRCagTgPejt1i. + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + lock_passwd: false + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxcXHmwaGnJ8scJaEN5RzklBPZpVSic4GdaAsKjQoeA your_email@example.com +runcmd: + - systemctl enable --now qemu-guest-agent.service +` + // Main+ClusterNetwork: do not add static IP here — the second interface (ClusterNetwork) + // often appears after cloud-init runs. The test adds the IP via SSH in EnsureStaticIPOnSecondInterface. + return base +} + +func (t *VMAdditionalNetworkTest) CheckCloudInitAdditionalNetworkCompleted(timeout time.Duration) { + GinkgoHelper() + + if !t.Config.HasMainNetwork { + // Only ClusterNetwork: IP comes from SDN. Wait until both VMs have status.IPAddress. + // If the environment does not set IPAddress for VMs without Main network, skip the test. + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + err := t.Framework.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VMFoo), t.VMFoo) + Expect(err).NotTo(HaveOccurred()) + err = t.Framework.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VMBar), t.VMBar) + Expect(err).NotTo(HaveOccurred()) + if t.VMFoo.Status.IPAddress != "" && t.VMBar.Status.IPAddress != "" { + return + } + time.Sleep(time.Second) + } + if t.VMFoo.Status.IPAddress == "" || t.VMBar.Status.IPAddress == "" { + Skip("VM status.IPAddress is not set for only-ClusterNetwork; SDN may not populate it in this environment") + } + return + } + // Main+ClusterNetwork: second interface (ClusterNetwork) appears after boot. Add static IPs via SSH with retries. + t.EnsureStaticIPOnSecondInterface(t.VMFoo, vmFooAdditionalIP, timeout) + t.EnsureStaticIPOnSecondInterface(t.VMBar, vmBarAdditionalIP, timeout) +} + +// EnsureStaticIPOnSecondInterface adds the given IP to the second (ClusterNetwork) interface via SSH. +// Retries until the second interface exists and the IP is present (it may appear some time after NetworkReady). +// The script is escaped for the outer shell that wraps the command in single quotes (d8 ssh -c '...'). +func (t *VMAdditionalNetworkTest) EnsureStaticIPOnSecondInterface(vm *v1alpha2.VirtualMachine, ip string, timeout time.Duration) { + GinkgoHelper() + + script := fmt.Sprintf( + `SECOND_IF=$(ip -o link show | awk -F': ' '{print $2}' | grep -v lo | sed -n '2p'); `+ + `if [ -n "$SECOND_IF" ]; then sudo ip addr add %s/24 dev $SECOND_IF 2>/dev/null; fi; `+ + `ip -4 addr show`, + ip, + ) + escapedScript := escapeCommandForSSH(script) + sshOpts := []framework.SSHCommandOption{framework.WithSSHTimeout(framework.LongTimeout)} + Eventually(func(g Gomega) { + cmdOut, err := t.Framework.SSHCommand(vm.Name, vm.Namespace, escapedScript, sshOpts...) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(cmdOut).To(ContainSubstring(fmt.Sprintf("inet %s", ip)), + "VM %s should have IP %s on second interface (ClusterNetwork)", vm.Name, ip) + }).WithTimeout(timeout).WithPolling(5 * time.Second).Should(Succeed()) +} + +// escapeCommandForSSH escapes a command so it can be passed inside single quotes to "ssh -c '...'". +// Replaces each ' with '\'' (end quote, literal quote, start quote). +func escapeCommandForSSH(cmd string) string { + return strings.ReplaceAll(cmd, "'", "'\\''") +} + +// CheckVMConnectivityBetweenVMs verifies that vm-foo and vm-bar can ping each other. +// For Main+ClusterNetwork uses static IPs 192.168.1.10/11 and retries (second interface/ARP may be delayed). +func (t *VMAdditionalNetworkTest) CheckVMConnectivityBetweenVMs() { + GinkgoHelper() + + if t.Config.HasMainNetwork { + // Retry: connectivity over the additional network may need a moment after IPs are added. + Eventually(func(g Gomega) { + cmdOut, err := t.runPing(t.VMFoo, vmBarAdditionalIP) + g.Expect(err).NotTo(HaveOccurred(), "ping vm-foo -> 192.168.1.11: %s", cmdOut) + g.Expect(cmdOut).To(ContainSubstring("0% packet loss")) + + cmdOut, err = t.runPing(t.VMBar, vmFooAdditionalIP) + g.Expect(err).NotTo(HaveOccurred(), "ping vm-bar -> 192.168.1.10: %s", cmdOut) + g.Expect(cmdOut).To(ContainSubstring("0% packet loss")) + }).WithTimeout(2 * framework.MiddleTimeout).WithPolling(10 * time.Second).Should(Succeed()) + return + } + // Only ClusterNetwork: get IPs from status (assigned by SDN). + err := t.Framework.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VMFoo), t.VMFoo) + Expect(err).NotTo(HaveOccurred()) + err = t.Framework.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VMBar), t.VMBar) + Expect(err).NotTo(HaveOccurred()) + Expect(t.VMFoo.Status.IPAddress).NotTo(BeEmpty(), "vm-foo must have status.IPAddress") + Expect(t.VMBar.Status.IPAddress).NotTo(BeEmpty(), "vm-bar must have status.IPAddress") + // Strip CIDR suffix if present (e.g. 10.66.10.3/32 -> 10.66.10.3). + fooIP := strings.Split(t.VMFoo.Status.IPAddress, "/")[0] + barIP := strings.Split(t.VMBar.Status.IPAddress, "/")[0] + t.CheckVMConnectivityToTargetIP(t.VMFoo, barIP) + t.CheckVMConnectivityToTargetIP(t.VMBar, fooIP) +} + +// runPing runs ping from the given VM to targetIP and returns (stdout, error). Does not call Expect. +func (t *VMAdditionalNetworkTest) runPing(vm *v1alpha2.VirtualMachine, targetIP string) (string, error) { + cmd := fmt.Sprintf("ping -c 2 -W 2 -w 5 %s", targetIP) + return t.Framework.SSHCommand(vm.Name, vm.Namespace, cmd, framework.WithSSHTimeout(framework.MiddleTimeout)) +} + +func (t *VMAdditionalNetworkTest) CheckVMConnectivityToTargetIP(vm *v1alpha2.VirtualMachine, targetIP string) { + GinkgoHelper() + + By(fmt.Sprintf("VM %q should have connectivity to %s", vm.Name, targetIP)) + cmdOut, err := t.runPing(vm, targetIP) + Expect(err).NotTo(HaveOccurred(), "ping from %s to %s failed: %s", vm.Name, targetIP, cmdOut) + Expect(cmdOut).To(ContainSubstring("0% packet loss"), "expected 0%% packet loss when pinging %s from %s, got: %s", targetIP, vm.Name, cmdOut) +} From 021eb5473ff00ed85163f96f3bdd3e8d7f469327 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Mon, 9 Feb 2026 09:45:39 +0200 Subject: [PATCH 2/7] Revert "feat(e2e): add vm without main network test" This reverts commit 6f6407e24593ffc157b16a8c5ce3446f1e101a64. Signed-off-by: Daniil Antoshin --- test/e2e/internal/util/sdn.go | 12 - test/e2e/legacy/vm_vpc.go | 204 ++++++++++++ test/e2e/vm/additional_network_interfaces.go | 331 ------------------- 3 files changed, 204 insertions(+), 343 deletions(-) create mode 100644 test/e2e/legacy/vm_vpc.go delete mode 100644 test/e2e/vm/additional_network_interfaces.go diff --git a/test/e2e/internal/util/sdn.go b/test/e2e/internal/util/sdn.go index f411cc8014..74dbd39f24 100644 --- a/test/e2e/internal/util/sdn.go +++ b/test/e2e/internal/util/sdn.go @@ -58,18 +58,6 @@ func IsSdnModuleEnabled(f *framework.Framework) bool { return enabled != nil && *enabled } -// IsSdnModuleEnabledOrError returns whether SDN module is enabled, or an error if config cannot be read. -// Used by legacy tests that do not have a framework instance. -func IsSdnModuleEnabledOrError() (bool, error) { - f := framework.NewFramework("") - sdnModule, err := f.GetModuleConfig("sdn") - if err != nil { - return false, err - } - enabled := sdnModule.Spec.Enabled - return enabled != nil && *enabled, nil -} - func IsClusterNetworkExists(f *framework.Framework) bool { GinkgoHelper() diff --git a/test/e2e/legacy/vm_vpc.go b/test/e2e/legacy/vm_vpc.go new file mode 100644 index 0000000000..b89a182ce4 --- /dev/null +++ b/test/e2e/legacy/vm_vpc.go @@ -0,0 +1,204 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package legacy + +import ( + "fmt" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" + "github.com/deckhouse/virtualization/test/e2e/internal/config" + "github.com/deckhouse/virtualization/test/e2e/internal/framework" + kc "github.com/deckhouse/virtualization/test/e2e/internal/kubectl" +) + +func WaitForVMNetworkReady(opts kc.WaitOptions) { + GinkgoHelper() + WaitConditionIsTrueByLabel(kc.ResourceVM, vmcondition.TypeNetworkReady.String(), opts) +} + +func WaitForVMRunningPhase(opts kc.WaitOptions) { + GinkgoHelper() + WaitPhaseByLabel(kc.ResourceVM, PhaseRunning, opts) +} + +var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", Ordered, func() { + testCaseLabel := map[string]string{"testcase": "vm-vpc"} + var ns string + + BeforeAll(func() { + sdnEnabled, err := isSdnModuleEnabled() + if err != nil || !sdnEnabled { + Skip("Module SDN is disabled. Skipping all tests for module SDN.") + } + + kustomization := fmt.Sprintf("%s/%s", conf.TestData.VMVpc, "kustomization.yaml") + ns, err = kustomize.GetNamespace(kustomization) + Expect(err).NotTo(HaveOccurred(), "%w", err) + + CreateNamespace(ns) + }) + + AfterAll(func() { + if CurrentSpecReport().Failed() { + SaveTestCaseDump(testCaseLabel, CurrentSpecReport().LeafNodeText, ns) + } + }) + + Context("When resources are applied", func() { + It("result should be succeeded", func() { + res := kubectl.Apply(kc.ApplyOptions{ + Filename: []string{conf.TestData.VMVpc}, + FilenameOption: kc.Kustomize, + }) + Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) + }) + }) + + Context("When virtual machines are applied", func() { + It("checks VMs phases", func() { + By("Virtual machine should be running") + WaitForVMRunningPhase(kc.WaitOptions{ + Labels: testCaseLabel, + Namespace: ns, + Timeout: MaxWaitTimeout, + }) + }) + It("checks network availability", func() { + By("Network condition should be true") + WaitForVMNetworkReady(kc.WaitOptions{ + Labels: testCaseLabel, + Namespace: ns, + Timeout: MaxWaitTimeout, + }) + + CheckVMConnectivityToTargetIPs(ns, testCaseLabel) + }) + }) + + Context("When virtual machine agents and network are ready", func() { + It("starts migrations", func() { + res := kubectl.List(kc.ResourceVM, kc.GetOptions{ + Labels: testCaseLabel, + Namespace: ns, + Output: "jsonpath='{.items[*].metadata.name}'", + }) + Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) + + vms := strings.Split(res.StdOut(), " ") + MigrateVirtualMachines(testCaseLabel, ns, vms...) + }) + }) + + Context("When VMs migrations are applied", func() { + It("checks VMs and VMOPs phases", func() { + By(fmt.Sprintf("VMOPs should be in %s phases", v1alpha2.VMOPPhaseCompleted)) + WaitPhaseByLabel(kc.ResourceVMOP, string(v1alpha2.VMOPPhaseCompleted), kc.WaitOptions{ + Labels: testCaseLabel, + Namespace: ns, + Timeout: MaxWaitTimeout, + }) + By("Virtual machines should be migrated") + WaitByLabel(kc.ResourceVM, kc.WaitOptions{ + Labels: testCaseLabel, + Namespace: ns, + Timeout: MaxWaitTimeout, + For: "'jsonpath={.status.migrationState.result}=Succeeded'", + }) + }) + + It("checks VMs external connection after migrations", func() { + res := kubectl.List(kc.ResourceVM, kc.GetOptions{ + Labels: testCaseLabel, + Namespace: ns, + Output: "jsonpath='{.items[*].metadata.name}'", + }) + Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) + + vms := strings.Split(res.StdOut(), " ") + Expect(vms).NotTo(BeEmpty()) + + // There is a known issue with the Cilium agent check. + CheckCiliumAgents(kubectl, ns, vms...) + CheckExternalConnection(externalHost, httpStatusOk, ns, vms...) + }) + + It("checks network availability after migrations", func() { + By("Network condition should be true") + WaitForVMNetworkReady(kc.WaitOptions{ + Labels: testCaseLabel, + Namespace: ns, + Timeout: MaxWaitTimeout, + }) + + CheckVMConnectivityToTargetIPs(ns, testCaseLabel) + }) + }) + + Context("When test is completed", func() { + It("deletes test case resources", func() { + resourcesToDelete := ResourcesToDelete{ + AdditionalResources: []AdditionalResource{ + { + Resource: kc.ResourceVMOP, + Labels: testCaseLabel, + }, + }, + } + + if config.IsCleanUpNeeded() { + resourcesToDelete.KustomizationDir = conf.TestData.VMVpc + } + + DeleteTestCaseResources(ns, resourcesToDelete) + }) + }) +}) + +func isSdnModuleEnabled() (bool, error) { + sdnModule, err := framework.NewFramework("").GetModuleConfig("sdn") + if err != nil { + return false, err + } + enabled := sdnModule.Spec.Enabled + + return enabled != nil && *enabled, nil +} + +func CheckVMConnectivityToTargetIPs(ns string, testCaseLabel map[string]string) { + var vmList v1alpha2.VirtualMachineList + err := GetObjects(kc.ResourceVM, &vmList, kc.GetOptions{ + Labels: testCaseLabel, + Namespace: ns, + }) + Expect(err).ShouldNot(HaveOccurred()) + + for _, vm := range vmList.Items { + switch { + case strings.Contains(vm.Name, "foo"): + By(fmt.Sprintf("VM %q should have connectivity to 192.168.1.10 (target: vm-bar)", vm.Name)) + CheckResultSSHCommand(ns, vm.Name, `ping -c 2 -W 2 -w 5 -q 192.168.1.10 2>&1 | grep -o "[0-9]\+%\s*packet loss"`, "0% packet loss") + case strings.Contains(vm.Name, "bar"): + By(fmt.Sprintf("VM %q should have connectivity to 192.168.1.11 (target: vm-foo)", vm.Name)) + CheckResultSSHCommand(ns, vm.Name, `ping -c 2 -W 2 -w 5 -q 192.168.1.11 2>&1 | grep -o "[0-9]\+%\s*packet loss"`, "0% packet loss") + } + } +} diff --git a/test/e2e/vm/additional_network_interfaces.go b/test/e2e/vm/additional_network_interfaces.go deleted file mode 100644 index 31709d4f1e..0000000000 --- a/test/e2e/vm/additional_network_interfaces.go +++ /dev/null @@ -1,331 +0,0 @@ -/* -Copyright 2025 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package vm - -import ( - "context" - "fmt" - "strings" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - crclient "sigs.k8s.io/controller-runtime/pkg/client" - - vdbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vd" - vmbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vm" - vmopbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vmop" - "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" - "github.com/deckhouse/virtualization/test/e2e/internal/framework" - "github.com/deckhouse/virtualization/test/e2e/internal/network" - "github.com/deckhouse/virtualization/test/e2e/internal/object" - "github.com/deckhouse/virtualization/test/e2e/internal/util" -) - -const ( - vmFooAdditionalIP = "192.168.1.10" - vmBarAdditionalIP = "192.168.1.11" -) - -type NetworkConfig struct { - Name string - HasMainNetwork bool - CheckExternalConnectivity bool -} - -var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", func() { - var ( - f *framework.Framework - t *VMAdditionalNetworkTest - ) - - BeforeEach(func() { - f = framework.NewFramework("vm-additional-network") - DeferCleanup(f.After) - f.Before() - - if !util.IsSdnModuleEnabled(f) { - Skip("SDN module is disabled. Skipping tests for additional network interfaces.") - } - - Expect(util.IsClusterNetworkExists(f)).To(BeTrue(), - fmt.Sprintf("Cluster network does not exist. Apply it first: %s", util.ClusterNetworkCreateCommand)) - }) - - DescribeTable("checks VM additional network connectivity and migration", - func(cfg NetworkConfig) { - t = NewVMAdditionalNetworkTest(f, cfg) - - By("Environment preparation", func() { - t.GenerateEnvironmentResources() - err := f.CreateWithDeferredDeletion(context.Background(), t.VDFoo, t.VDBar, t.VMFoo, t.VMBar) - Expect(err).NotTo(HaveOccurred()) - - util.UntilObjectPhase(string(v1alpha2.MachineRunning), framework.LongTimeout, t.VMFoo, t.VMBar) - util.UntilVMAgentReady(crclient.ObjectKeyFromObject(t.VMFoo), framework.LongTimeout) - util.UntilVMAgentReady(crclient.ObjectKeyFromObject(t.VMBar), framework.LongTimeout) - util.UntilConditionStatus(vmcondition.TypeNetworkReady.String(), string(metav1.ConditionTrue), framework.LongTimeout, t.VMFoo, t.VMBar) - - t.CheckCloudInitAdditionalNetworkCompleted(framework.LongTimeout) - }) - - By("Check connectivity between VMs via additional network", func() { - t.CheckVMConnectivityBetweenVMs() - }) - - By("Trigger migrations", func() { - util.MigrateVirtualMachine(f, t.VMFoo, vmopbuilder.WithGenerateName("vmop-migrate-foo-")) - util.MigrateVirtualMachine(f, t.VMBar, vmopbuilder.WithGenerateName("vmop-migrate-bar-")) - }) - - By("Wait for migrations to complete", func() { - util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(t.VMFoo), framework.LongTimeout) - util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(t.VMBar), framework.LongTimeout) - }) - - By("Check Cilium agents after migration", func() { - err := network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), t.VMFoo.Name, f.Namespace().Name) - Expect(err).NotTo(HaveOccurred(), "Cilium agents check should succeed for VM %s", t.VMFoo.Name) - err = network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), t.VMBar.Name, f.Namespace().Name) - Expect(err).NotTo(HaveOccurred(), "Cilium agents check should succeed for VM %s", t.VMBar.Name) - }) - - if cfg.CheckExternalConnectivity { - By("Check external connectivity after migration", func() { - network.CheckExternalConnectivity(f, t.VMFoo.Name, network.ExternalHost, network.HTTPStatusOk) - network.CheckExternalConnectivity(f, t.VMBar.Name, network.ExternalHost, network.HTTPStatusOk) - }) - } - - By("Check network condition after migration", func() { - util.UntilConditionStatus(vmcondition.TypeNetworkReady.String(), string(metav1.ConditionTrue), framework.LongTimeout, t.VMFoo, t.VMBar) - }) - - By("Check connectivity between VMs via additional network after migration", func() { - t.CheckVMConnectivityBetweenVMs() - }) - }, - Entry("with Main and ClusterNetwork", NetworkConfig{Name: "with Main and ClusterNetwork", HasMainNetwork: true, CheckExternalConnectivity: true}), - Entry("only ClusterNetwork", NetworkConfig{Name: "only ClusterNetwork", HasMainNetwork: false, CheckExternalConnectivity: false}), - ) -}) - -type VMAdditionalNetworkTest struct { - Framework *framework.Framework - Config NetworkConfig - - VDFoo *v1alpha2.VirtualDisk - VDBar *v1alpha2.VirtualDisk - VMFoo *v1alpha2.VirtualMachine - VMBar *v1alpha2.VirtualMachine -} - -func NewVMAdditionalNetworkTest(f *framework.Framework, cfg NetworkConfig) *VMAdditionalNetworkTest { - return &VMAdditionalNetworkTest{Framework: f, Config: cfg} -} - -func (t *VMAdditionalNetworkTest) GenerateEnvironmentResources() { - t.VDFoo = vdbuilder.New( - vdbuilder.WithName("vd-foo-root"), - vdbuilder.WithNamespace(t.Framework.Namespace().Name), - vdbuilder.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ - URL: object.ImageURLUbuntu, - }), - ) - - t.VDBar = vdbuilder.New( - vdbuilder.WithName("vd-bar-root"), - vdbuilder.WithNamespace(t.Framework.Namespace().Name), - vdbuilder.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ - URL: object.ImageURLUbuntu, - }), - ) - - t.VMFoo = t.buildVM("vm-foo", t.VDFoo.Name, t.getCloudInitFoo()) - t.VMBar = t.buildVM("vm-bar", t.VDBar.Name, t.getCloudInitBar()) -} - -func (t *VMAdditionalNetworkTest) buildVM(name, vdName, cloudInit string) *v1alpha2.VirtualMachine { - opts := []vmbuilder.Option{ - vmbuilder.WithName(name), - vmbuilder.WithNamespace(t.Framework.Namespace().Name), - vmbuilder.WithCPU(1, ptr.To("50%")), - vmbuilder.WithMemory(resource.MustParse("256Mi")), - vmbuilder.WithLiveMigrationPolicy(v1alpha2.AlwaysSafeMigrationPolicy), - vmbuilder.WithVirtualMachineClass(object.DefaultVMClass), - vmbuilder.WithProvisioningUserData(cloudInit), - vmbuilder.WithBlockDeviceRefs( - v1alpha2.BlockDeviceSpecRef{ - Kind: v1alpha2.DiskDevice, - Name: vdName, - }, - ), - } - if t.Config.HasMainNetwork { - opts = append(opts, - vmbuilder.WithNetwork(v1alpha2.NetworksSpec{Type: v1alpha2.NetworksTypeMain}), - vmbuilder.WithNetwork(v1alpha2.NetworksSpec{ - Type: v1alpha2.NetworksTypeClusterNetwork, - Name: util.ClusterNetworkName, - }), - ) - } else { - opts = append(opts, vmbuilder.WithNetwork(v1alpha2.NetworksSpec{ - Type: v1alpha2.NetworksTypeClusterNetwork, - Name: util.ClusterNetworkName, - })) - } - return vmbuilder.New(opts...) -} - -func (t *VMAdditionalNetworkTest) getCloudInitFoo() string { - return t.getCloudInitWithAdditionalIP(vmFooAdditionalIP) -} - -func (t *VMAdditionalNetworkTest) getCloudInitBar() string { - return t.getCloudInitWithAdditionalIP(vmBarAdditionalIP) -} - -// getCloudInitWithAdditionalIP returns cloud-init. For "Main+ClusterNetwork" it configures the second -// interface with the given static IP. For "only ClusterNetwork" it only installs qemu-guest-agent; -// the single interface gets its IP from SDN (status.IPAddress). -func (t *VMAdditionalNetworkTest) getCloudInitWithAdditionalIP(additionalIP string) string { - base := `#cloud-config -package_update: true -packages: - - qemu-guest-agent -users: - - name: cloud - passwd: $6$rounds=4096$vln/.aPHBOI7BMYR$bBMkqQvuGs5Gyd/1H5DP4m9HjQSy.kgrxpaGEHwkX7KEFV8BS.HZWPitAtZ2Vd8ZqIZRqmlykRCagTgPejt1i. - shell: /bin/bash - sudo: ALL=(ALL) NOPASSWD:ALL - lock_passwd: false - ssh_authorized_keys: - - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxcXHmwaGnJ8scJaEN5RzklBPZpVSic4GdaAsKjQoeA your_email@example.com -runcmd: - - systemctl enable --now qemu-guest-agent.service -` - // Main+ClusterNetwork: do not add static IP here — the second interface (ClusterNetwork) - // often appears after cloud-init runs. The test adds the IP via SSH in EnsureStaticIPOnSecondInterface. - return base -} - -func (t *VMAdditionalNetworkTest) CheckCloudInitAdditionalNetworkCompleted(timeout time.Duration) { - GinkgoHelper() - - if !t.Config.HasMainNetwork { - // Only ClusterNetwork: IP comes from SDN. Wait until both VMs have status.IPAddress. - // If the environment does not set IPAddress for VMs without Main network, skip the test. - deadline := time.Now().Add(timeout) - for time.Now().Before(deadline) { - err := t.Framework.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VMFoo), t.VMFoo) - Expect(err).NotTo(HaveOccurred()) - err = t.Framework.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VMBar), t.VMBar) - Expect(err).NotTo(HaveOccurred()) - if t.VMFoo.Status.IPAddress != "" && t.VMBar.Status.IPAddress != "" { - return - } - time.Sleep(time.Second) - } - if t.VMFoo.Status.IPAddress == "" || t.VMBar.Status.IPAddress == "" { - Skip("VM status.IPAddress is not set for only-ClusterNetwork; SDN may not populate it in this environment") - } - return - } - // Main+ClusterNetwork: second interface (ClusterNetwork) appears after boot. Add static IPs via SSH with retries. - t.EnsureStaticIPOnSecondInterface(t.VMFoo, vmFooAdditionalIP, timeout) - t.EnsureStaticIPOnSecondInterface(t.VMBar, vmBarAdditionalIP, timeout) -} - -// EnsureStaticIPOnSecondInterface adds the given IP to the second (ClusterNetwork) interface via SSH. -// Retries until the second interface exists and the IP is present (it may appear some time after NetworkReady). -// The script is escaped for the outer shell that wraps the command in single quotes (d8 ssh -c '...'). -func (t *VMAdditionalNetworkTest) EnsureStaticIPOnSecondInterface(vm *v1alpha2.VirtualMachine, ip string, timeout time.Duration) { - GinkgoHelper() - - script := fmt.Sprintf( - `SECOND_IF=$(ip -o link show | awk -F': ' '{print $2}' | grep -v lo | sed -n '2p'); `+ - `if [ -n "$SECOND_IF" ]; then sudo ip addr add %s/24 dev $SECOND_IF 2>/dev/null; fi; `+ - `ip -4 addr show`, - ip, - ) - escapedScript := escapeCommandForSSH(script) - sshOpts := []framework.SSHCommandOption{framework.WithSSHTimeout(framework.LongTimeout)} - Eventually(func(g Gomega) { - cmdOut, err := t.Framework.SSHCommand(vm.Name, vm.Namespace, escapedScript, sshOpts...) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(cmdOut).To(ContainSubstring(fmt.Sprintf("inet %s", ip)), - "VM %s should have IP %s on second interface (ClusterNetwork)", vm.Name, ip) - }).WithTimeout(timeout).WithPolling(5 * time.Second).Should(Succeed()) -} - -// escapeCommandForSSH escapes a command so it can be passed inside single quotes to "ssh -c '...'". -// Replaces each ' with '\'' (end quote, literal quote, start quote). -func escapeCommandForSSH(cmd string) string { - return strings.ReplaceAll(cmd, "'", "'\\''") -} - -// CheckVMConnectivityBetweenVMs verifies that vm-foo and vm-bar can ping each other. -// For Main+ClusterNetwork uses static IPs 192.168.1.10/11 and retries (second interface/ARP may be delayed). -func (t *VMAdditionalNetworkTest) CheckVMConnectivityBetweenVMs() { - GinkgoHelper() - - if t.Config.HasMainNetwork { - // Retry: connectivity over the additional network may need a moment after IPs are added. - Eventually(func(g Gomega) { - cmdOut, err := t.runPing(t.VMFoo, vmBarAdditionalIP) - g.Expect(err).NotTo(HaveOccurred(), "ping vm-foo -> 192.168.1.11: %s", cmdOut) - g.Expect(cmdOut).To(ContainSubstring("0% packet loss")) - - cmdOut, err = t.runPing(t.VMBar, vmFooAdditionalIP) - g.Expect(err).NotTo(HaveOccurred(), "ping vm-bar -> 192.168.1.10: %s", cmdOut) - g.Expect(cmdOut).To(ContainSubstring("0% packet loss")) - }).WithTimeout(2 * framework.MiddleTimeout).WithPolling(10 * time.Second).Should(Succeed()) - return - } - // Only ClusterNetwork: get IPs from status (assigned by SDN). - err := t.Framework.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VMFoo), t.VMFoo) - Expect(err).NotTo(HaveOccurred()) - err = t.Framework.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VMBar), t.VMBar) - Expect(err).NotTo(HaveOccurred()) - Expect(t.VMFoo.Status.IPAddress).NotTo(BeEmpty(), "vm-foo must have status.IPAddress") - Expect(t.VMBar.Status.IPAddress).NotTo(BeEmpty(), "vm-bar must have status.IPAddress") - // Strip CIDR suffix if present (e.g. 10.66.10.3/32 -> 10.66.10.3). - fooIP := strings.Split(t.VMFoo.Status.IPAddress, "/")[0] - barIP := strings.Split(t.VMBar.Status.IPAddress, "/")[0] - t.CheckVMConnectivityToTargetIP(t.VMFoo, barIP) - t.CheckVMConnectivityToTargetIP(t.VMBar, fooIP) -} - -// runPing runs ping from the given VM to targetIP and returns (stdout, error). Does not call Expect. -func (t *VMAdditionalNetworkTest) runPing(vm *v1alpha2.VirtualMachine, targetIP string) (string, error) { - cmd := fmt.Sprintf("ping -c 2 -W 2 -w 5 %s", targetIP) - return t.Framework.SSHCommand(vm.Name, vm.Namespace, cmd, framework.WithSSHTimeout(framework.MiddleTimeout)) -} - -func (t *VMAdditionalNetworkTest) CheckVMConnectivityToTargetIP(vm *v1alpha2.VirtualMachine, targetIP string) { - GinkgoHelper() - - By(fmt.Sprintf("VM %q should have connectivity to %s", vm.Name, targetIP)) - cmdOut, err := t.runPing(vm, targetIP) - Expect(err).NotTo(HaveOccurred(), "ping from %s to %s failed: %s", vm.Name, targetIP, cmdOut) - Expect(cmdOut).To(ContainSubstring("0% packet loss"), "expected 0%% packet loss when pinging %s from %s, got: %s", targetIP, vm.Name, cmdOut) -} From 931e8b6bf07099a625d2f212a04e236e630aac70 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Mon, 9 Feb 2026 11:51:32 +0200 Subject: [PATCH 3/7] return and rewrite Signed-off-by: Daniil Antoshin --- test/e2e/legacy/vm_vpc.go | 174 +------------- test/e2e/vm/additional_network_interfaces.go | 240 +++++++++++++++++++ 2 files changed, 241 insertions(+), 173 deletions(-) create mode 100644 test/e2e/vm/additional_network_interfaces.go diff --git a/test/e2e/legacy/vm_vpc.go b/test/e2e/legacy/vm_vpc.go index b89a182ce4..6a895b1010 100644 --- a/test/e2e/legacy/vm_vpc.go +++ b/test/e2e/legacy/vm_vpc.go @@ -17,162 +17,10 @@ limitations under the License. package legacy import ( - "fmt" - "strings" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" - "github.com/deckhouse/virtualization/test/e2e/internal/config" "github.com/deckhouse/virtualization/test/e2e/internal/framework" - kc "github.com/deckhouse/virtualization/test/e2e/internal/kubectl" ) -func WaitForVMNetworkReady(opts kc.WaitOptions) { - GinkgoHelper() - WaitConditionIsTrueByLabel(kc.ResourceVM, vmcondition.TypeNetworkReady.String(), opts) -} - -func WaitForVMRunningPhase(opts kc.WaitOptions) { - GinkgoHelper() - WaitPhaseByLabel(kc.ResourceVM, PhaseRunning, opts) -} - -var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", Ordered, func() { - testCaseLabel := map[string]string{"testcase": "vm-vpc"} - var ns string - - BeforeAll(func() { - sdnEnabled, err := isSdnModuleEnabled() - if err != nil || !sdnEnabled { - Skip("Module SDN is disabled. Skipping all tests for module SDN.") - } - - kustomization := fmt.Sprintf("%s/%s", conf.TestData.VMVpc, "kustomization.yaml") - ns, err = kustomize.GetNamespace(kustomization) - Expect(err).NotTo(HaveOccurred(), "%w", err) - - CreateNamespace(ns) - }) - - AfterAll(func() { - if CurrentSpecReport().Failed() { - SaveTestCaseDump(testCaseLabel, CurrentSpecReport().LeafNodeText, ns) - } - }) - - Context("When resources are applied", func() { - It("result should be succeeded", func() { - res := kubectl.Apply(kc.ApplyOptions{ - Filename: []string{conf.TestData.VMVpc}, - FilenameOption: kc.Kustomize, - }) - Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) - }) - }) - - Context("When virtual machines are applied", func() { - It("checks VMs phases", func() { - By("Virtual machine should be running") - WaitForVMRunningPhase(kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - It("checks network availability", func() { - By("Network condition should be true") - WaitForVMNetworkReady(kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - - CheckVMConnectivityToTargetIPs(ns, testCaseLabel) - }) - }) - - Context("When virtual machine agents and network are ready", func() { - It("starts migrations", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: testCaseLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) - - vms := strings.Split(res.StdOut(), " ") - MigrateVirtualMachines(testCaseLabel, ns, vms...) - }) - }) - - Context("When VMs migrations are applied", func() { - It("checks VMs and VMOPs phases", func() { - By(fmt.Sprintf("VMOPs should be in %s phases", v1alpha2.VMOPPhaseCompleted)) - WaitPhaseByLabel(kc.ResourceVMOP, string(v1alpha2.VMOPPhaseCompleted), kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - By("Virtual machines should be migrated") - WaitByLabel(kc.ResourceVM, kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - For: "'jsonpath={.status.migrationState.result}=Succeeded'", - }) - }) - - It("checks VMs external connection after migrations", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: testCaseLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) - - vms := strings.Split(res.StdOut(), " ") - Expect(vms).NotTo(BeEmpty()) - - // There is a known issue with the Cilium agent check. - CheckCiliumAgents(kubectl, ns, vms...) - CheckExternalConnection(externalHost, httpStatusOk, ns, vms...) - }) - - It("checks network availability after migrations", func() { - By("Network condition should be true") - WaitForVMNetworkReady(kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - - CheckVMConnectivityToTargetIPs(ns, testCaseLabel) - }) - }) - - Context("When test is completed", func() { - It("deletes test case resources", func() { - resourcesToDelete := ResourcesToDelete{ - AdditionalResources: []AdditionalResource{ - { - Resource: kc.ResourceVMOP, - Labels: testCaseLabel, - }, - }, - } - - if config.IsCleanUpNeeded() { - resourcesToDelete.KustomizationDir = conf.TestData.VMVpc - } - - DeleteTestCaseResources(ns, resourcesToDelete) - }) - }) -}) - +// isSdnModuleEnabled is used by legacy restore tests (vm_restore_safe, vm_restore_force). func isSdnModuleEnabled() (bool, error) { sdnModule, err := framework.NewFramework("").GetModuleConfig("sdn") if err != nil { @@ -182,23 +30,3 @@ func isSdnModuleEnabled() (bool, error) { return enabled != nil && *enabled, nil } - -func CheckVMConnectivityToTargetIPs(ns string, testCaseLabel map[string]string) { - var vmList v1alpha2.VirtualMachineList - err := GetObjects(kc.ResourceVM, &vmList, kc.GetOptions{ - Labels: testCaseLabel, - Namespace: ns, - }) - Expect(err).ShouldNot(HaveOccurred()) - - for _, vm := range vmList.Items { - switch { - case strings.Contains(vm.Name, "foo"): - By(fmt.Sprintf("VM %q should have connectivity to 192.168.1.10 (target: vm-bar)", vm.Name)) - CheckResultSSHCommand(ns, vm.Name, `ping -c 2 -W 2 -w 5 -q 192.168.1.10 2>&1 | grep -o "[0-9]\+%\s*packet loss"`, "0% packet loss") - case strings.Contains(vm.Name, "bar"): - By(fmt.Sprintf("VM %q should have connectivity to 192.168.1.11 (target: vm-foo)", vm.Name)) - CheckResultSSHCommand(ns, vm.Name, `ping -c 2 -W 2 -w 5 -q 192.168.1.11 2>&1 | grep -o "[0-9]\+%\s*packet loss"`, "0% packet loss") - } - } -} diff --git a/test/e2e/vm/additional_network_interfaces.go b/test/e2e/vm/additional_network_interfaces.go new file mode 100644 index 0000000000..230f3af78c --- /dev/null +++ b/test/e2e/vm/additional_network_interfaces.go @@ -0,0 +1,240 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vm + +import ( + "context" + "fmt" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/utils/ptr" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/builder/vd" + "github.com/deckhouse/virtualization-controller/pkg/builder/vm" + "github.com/deckhouse/virtualization-controller/pkg/builder/vmop" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" + "github.com/deckhouse/virtualization/test/e2e/internal/framework" + "github.com/deckhouse/virtualization/test/e2e/internal/network" + "github.com/deckhouse/virtualization/test/e2e/internal/object" + "github.com/deckhouse/virtualization/test/e2e/internal/util" +) + +const ( + // IPs on additional interface (eth1) for connectivity check between VMs. + vmFooAdditionalIP = "192.168.1.10" + vmBarAdditionalIP = "192.168.1.11" +) + +var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", func() { + var ( + vdFooRoot *v1alpha2.VirtualDisk + vdBarRoot *v1alpha2.VirtualDisk + vmFoo *v1alpha2.VirtualMachine + vmBar *v1alpha2.VirtualMachine + + f = framework.NewFramework("vm-additional-network") + ) + + BeforeEach(func() { + DeferCleanup(f.After) + + f.Before() + + if !util.IsSdnModuleEnabled(f) { + Skip("SDN module is disabled. Skipping test.") + } + + Expect(util.IsClusterNetworkExists(f)).To(BeTrue(), + fmt.Sprintf("Cluster network %s does not exist. Create it first: %s", util.ClusterNetworkName, util.ClusterNetworkCreateCommand)) + }) + + It("verifies additional network interfaces and connectivity before and after migration", func() { + By("Environment preparation", func() { + ns := f.Namespace().Name + + vdFooRoot = vd.New( + vd.WithName("vd-foo-root"), + vd.WithNamespace(ns), + vd.WithSize(ptr.To(resource.MustParse("512Mi"))), + vd.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ + URL: object.ImageURLAlpineUEFIPerf, + }), + ) + vdBarRoot = vd.New( + vd.WithName("vd-bar-root"), + vd.WithNamespace(ns), + vd.WithSize(ptr.To(resource.MustParse("512Mi"))), + vd.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ + URL: object.ImageURLAlpineUEFIPerf, + }), + ) + + vmFoo = vm.New( + vm.WithName("vm-foo"), + vm.WithNamespace(ns), + vm.WithBootloader(v1alpha2.EFI), + vm.WithCPU(1, ptr.To("5%")), + vm.WithMemory(resource.MustParse("256Mi")), + vm.WithRestartApprovalMode(v1alpha2.Manual), + vm.WithVirtualMachineClass(object.DefaultVMClass), + vm.WithLiveMigrationPolicy(v1alpha2.PreferSafeMigrationPolicy), + vm.WithProvisioningUserData(cloudInitAdditionalNetwork(vmFooAdditionalIP)), + vm.WithBlockDeviceRefs(v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.VirtualDiskKind, + Name: vdFooRoot.Name, + }), + vm.WithNetwork(v1alpha2.NetworksSpec{Type: v1alpha2.NetworksTypeMain}), + vm.WithNetwork(v1alpha2.NetworksSpec{ + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: util.ClusterNetworkName, + }), + ) + vmBar = vm.New( + vm.WithName("vm-bar"), + vm.WithNamespace(ns), + vm.WithBootloader(v1alpha2.EFI), + vm.WithCPU(1, ptr.To("5%")), + vm.WithMemory(resource.MustParse("256Mi")), + vm.WithRestartApprovalMode(v1alpha2.Manual), + vm.WithVirtualMachineClass(object.DefaultVMClass), + vm.WithLiveMigrationPolicy(v1alpha2.PreferSafeMigrationPolicy), + vm.WithProvisioningUserData(cloudInitAdditionalNetwork(vmBarAdditionalIP)), + vm.WithBlockDeviceRefs(v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.VirtualDiskKind, + Name: vdBarRoot.Name, + }), + vm.WithNetwork(v1alpha2.NetworksSpec{Type: v1alpha2.NetworksTypeMain}), + vm.WithNetwork(v1alpha2.NetworksSpec{ + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: util.ClusterNetworkName, + }), + ) + + err := f.CreateWithDeferredDeletion(context.Background(), vdFooRoot, vdBarRoot, vmFoo, vmBar) + Expect(err).NotTo(HaveOccurred()) + + util.UntilObjectPhase(string(v1alpha2.MachineRunning), framework.LongTimeout, vmFoo, vmBar) + util.UntilVMAgentReady(crclient.ObjectKeyFromObject(vmFoo), framework.LongTimeout) + util.UntilVMAgentReady(crclient.ObjectKeyFromObject(vmBar), framework.LongTimeout) + }) + + By("Wait for additional network interfaces to be ready", func() { + util.UntilConditionStatus(vmcondition.TypeNetworkReady.String(), "True", framework.LongTimeout, vmFoo, vmBar) + }) + + By("Check connectivity between VMs via additional network", func() { + checkConnectivityBetweenVMs(f, vmFoo, vmBar) + }) + + By("Create VMOPs to trigger migration", func() { + util.MigrateVirtualMachine(f, vmFoo, vmop.WithGenerateName("vmop-migrate-foo-")) + util.MigrateVirtualMachine(f, vmBar, vmop.WithGenerateName("vmop-migrate-bar-")) + }) + + By("Wait for migration to complete", func() { + util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(vmFoo), framework.LongTimeout) + util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(vmBar), framework.LongTimeout) + }) + + By("Check Cilium agents after migration", func() { + err := network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), vmFoo.Name, f.Namespace().Name) + Expect(err).NotTo(HaveOccurred(), "Cilium agents check for VM %s", vmFoo.Name) + err = network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), vmBar.Name, f.Namespace().Name) + Expect(err).NotTo(HaveOccurred(), "Cilium agents check for VM %s", vmBar.Name) + }) + + By("Check VM can reach external network after migration", func() { + network.CheckExternalConnectivity(f, vmFoo.Name, network.ExternalHost, network.HTTPStatusOk) + network.CheckExternalConnectivity(f, vmBar.Name, network.ExternalHost, network.HTTPStatusOk) + }) + + By("Wait for additional network interfaces to be ready after migration", func() { + util.UntilConditionStatus(vmcondition.TypeNetworkReady.String(), "True", framework.LongTimeout, vmFoo, vmBar) + }) + + By("Check connectivity between VMs via additional network after migration", func() { + checkConnectivityBetweenVMs(f, vmFoo, vmBar) + }) + }) +}) + +// cloudInitAdditionalNetwork returns cloud-init that configures eth1 with the given static IP (Alpine /etc/network/interfaces). +func cloudInitAdditionalNetwork(eth1Address string) string { + return fmt.Sprintf(`#cloud-config +ssh_pwauth: True +users: + - name: cloud + passwd: $6$rounds=4096$vln/.aPHBOI7BMYR$bBMkqQvuGs5Gyd/1H5DP4m9HjQSy.kgrxpaGEHwkX7KEFV8BS.HZWPitAtZ2Vd8ZqIZRqmlykRCagTgPejt1i. + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + chpasswd: { expire: False } + lock_passwd: False + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxcXHmwaGnJ8scJaEN5RzklBPZpVSic4GdaAsKjQoeA your_email@example.com +packages: + - qemu-guest-agent +write_files: + - path: /etc/network/interfaces + append: true + content: | + + auto eth1 + iface eth1 inet static + address %s + netmask 255.255.255.0 +runcmd: + - sudo rc-update add qemu-guest-agent default + - sudo rc-service qemu-guest-agent start + - sudo /etc/init.d/networking restart + - chown -R cloud:cloud /home/cloud +`, eth1Address) +} + +func checkConnectivityBetweenVMs(f *framework.Framework, vmFoo, vmBar *v1alpha2.VirtualMachine) { + GinkgoHelper() + + pingCmd := "ping -c 2 -W 2 -w 5 -q %s 2>&1 | grep -o \"[0-9]\\+%%\\s*packet loss\"" // %% -> % in output + expectedOut := "0% packet loss" + + By(fmt.Sprintf("VM %s should have connectivity to %s (vm-bar)", vmFoo.Name, vmBarAdditionalIP)) + checkResultSSHCommand(f, vmFoo.Name, vmFoo.Namespace, fmt.Sprintf(pingCmd, vmBarAdditionalIP), expectedOut) + + By(fmt.Sprintf("VM %s should have connectivity to %s (vm-foo)", vmBar.Name, vmFooAdditionalIP)) + checkResultSSHCommand(f, vmBar.Name, vmBar.Namespace, fmt.Sprintf(pingCmd, vmFooAdditionalIP), expectedOut) +} + +const ( + Interval = 5 * time.Second + Timeout = 90 * time.Second +) + +func checkResultSSHCommand(f *framework.Framework, vmName, vmNamespace, cmd, equal string) { + GinkgoHelper() + Eventually(func() (string, error) { + res, err := f.SSHCommand(vmName, vmNamespace, cmd) + if err != nil { + return "", fmt.Errorf("cmd: %s\nstderr: %w", cmd, err) + } + return strings.TrimSpace(res), nil + }).WithTimeout(Timeout).WithPolling(Interval).Should(Equal(equal)) +} From 872ad40b96e29c34e1d00c5aaf59aa0405a00dcf Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Mon, 9 Feb 2026 12:13:41 +0200 Subject: [PATCH 4/7] add without main test Signed-off-by: Daniil Antoshin --- test/e2e/vm/additional_network_interfaces.go | 225 ++++++++++--------- 1 file changed, 117 insertions(+), 108 deletions(-) diff --git a/test/e2e/vm/additional_network_interfaces.go b/test/e2e/vm/additional_network_interfaces.go index 230f3af78c..626b4c749d 100644 --- a/test/e2e/vm/additional_network_interfaces.go +++ b/test/e2e/vm/additional_network_interfaces.go @@ -45,6 +45,10 @@ const ( vmBarAdditionalIP = "192.168.1.11" ) +type additionalNetworkTestCase struct { + vmBarHasMainNetwork bool +} + var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", func() { var ( vdFooRoot *v1alpha2.VirtualDisk @@ -68,116 +72,121 @@ var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", func() { fmt.Sprintf("Cluster network %s does not exist. Create it first: %s", util.ClusterNetworkName, util.ClusterNetworkCreateCommand)) }) - It("verifies additional network interfaces and connectivity before and after migration", func() { - By("Environment preparation", func() { - ns := f.Namespace().Name - - vdFooRoot = vd.New( - vd.WithName("vd-foo-root"), - vd.WithNamespace(ns), - vd.WithSize(ptr.To(resource.MustParse("512Mi"))), - vd.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ - URL: object.ImageURLAlpineUEFIPerf, - }), - ) - vdBarRoot = vd.New( - vd.WithName("vd-bar-root"), - vd.WithNamespace(ns), - vd.WithSize(ptr.To(resource.MustParse("512Mi"))), - vd.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ - URL: object.ImageURLAlpineUEFIPerf, - }), - ) - - vmFoo = vm.New( - vm.WithName("vm-foo"), - vm.WithNamespace(ns), - vm.WithBootloader(v1alpha2.EFI), - vm.WithCPU(1, ptr.To("5%")), - vm.WithMemory(resource.MustParse("256Mi")), - vm.WithRestartApprovalMode(v1alpha2.Manual), - vm.WithVirtualMachineClass(object.DefaultVMClass), - vm.WithLiveMigrationPolicy(v1alpha2.PreferSafeMigrationPolicy), - vm.WithProvisioningUserData(cloudInitAdditionalNetwork(vmFooAdditionalIP)), - vm.WithBlockDeviceRefs(v1alpha2.BlockDeviceSpecRef{ - Kind: v1alpha2.VirtualDiskKind, - Name: vdFooRoot.Name, - }), - vm.WithNetwork(v1alpha2.NetworksSpec{Type: v1alpha2.NetworksTypeMain}), - vm.WithNetwork(v1alpha2.NetworksSpec{ - Type: v1alpha2.NetworksTypeClusterNetwork, - Name: util.ClusterNetworkName, - }), - ) - vmBar = vm.New( - vm.WithName("vm-bar"), - vm.WithNamespace(ns), - vm.WithBootloader(v1alpha2.EFI), - vm.WithCPU(1, ptr.To("5%")), - vm.WithMemory(resource.MustParse("256Mi")), - vm.WithRestartApprovalMode(v1alpha2.Manual), - vm.WithVirtualMachineClass(object.DefaultVMClass), - vm.WithLiveMigrationPolicy(v1alpha2.PreferSafeMigrationPolicy), - vm.WithProvisioningUserData(cloudInitAdditionalNetwork(vmBarAdditionalIP)), - vm.WithBlockDeviceRefs(v1alpha2.BlockDeviceSpecRef{ - Kind: v1alpha2.VirtualDiskKind, - Name: vdBarRoot.Name, - }), - vm.WithNetwork(v1alpha2.NetworksSpec{Type: v1alpha2.NetworksTypeMain}), - vm.WithNetwork(v1alpha2.NetworksSpec{ - Type: v1alpha2.NetworksTypeClusterNetwork, - Name: util.ClusterNetworkName, - }), - ) - - err := f.CreateWithDeferredDeletion(context.Background(), vdFooRoot, vdBarRoot, vmFoo, vmBar) - Expect(err).NotTo(HaveOccurred()) - - util.UntilObjectPhase(string(v1alpha2.MachineRunning), framework.LongTimeout, vmFoo, vmBar) - util.UntilVMAgentReady(crclient.ObjectKeyFromObject(vmFoo), framework.LongTimeout) - util.UntilVMAgentReady(crclient.ObjectKeyFromObject(vmBar), framework.LongTimeout) - }) - - By("Wait for additional network interfaces to be ready", func() { - util.UntilConditionStatus(vmcondition.TypeNetworkReady.String(), "True", framework.LongTimeout, vmFoo, vmBar) - }) - - By("Check connectivity between VMs via additional network", func() { - checkConnectivityBetweenVMs(f, vmFoo, vmBar) - }) - - By("Create VMOPs to trigger migration", func() { - util.MigrateVirtualMachine(f, vmFoo, vmop.WithGenerateName("vmop-migrate-foo-")) - util.MigrateVirtualMachine(f, vmBar, vmop.WithGenerateName("vmop-migrate-bar-")) - }) - - By("Wait for migration to complete", func() { - util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(vmFoo), framework.LongTimeout) - util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(vmBar), framework.LongTimeout) - }) - - By("Check Cilium agents after migration", func() { - err := network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), vmFoo.Name, f.Namespace().Name) - Expect(err).NotTo(HaveOccurred(), "Cilium agents check for VM %s", vmFoo.Name) - err = network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), vmBar.Name, f.Namespace().Name) - Expect(err).NotTo(HaveOccurred(), "Cilium agents check for VM %s", vmBar.Name) - }) - - By("Check VM can reach external network after migration", func() { - network.CheckExternalConnectivity(f, vmFoo.Name, network.ExternalHost, network.HTTPStatusOk) - network.CheckExternalConnectivity(f, vmBar.Name, network.ExternalHost, network.HTTPStatusOk) - }) - - By("Wait for additional network interfaces to be ready after migration", func() { - util.UntilConditionStatus(vmcondition.TypeNetworkReady.String(), "True", framework.LongTimeout, vmFoo, vmBar) - }) - - By("Check connectivity between VMs via additional network after migration", func() { - checkConnectivityBetweenVMs(f, vmFoo, vmBar) - }) - }) + DescribeTable("verifies additional network interfaces and connectivity before and after migration", + func(tc additionalNetworkTestCase) { + By("Environment preparation", func() { + ns := f.Namespace().Name + + vdFooRoot = vd.New( + vd.WithName("vd-foo-root"), + vd.WithNamespace(ns), + vd.WithSize(ptr.To(resource.MustParse("512Mi"))), + vd.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ + URL: object.ImageURLAlpineUEFIPerf, + }), + ) + vdBarRoot = vd.New( + vd.WithName("vd-bar-root"), + vd.WithNamespace(ns), + vd.WithSize(ptr.To(resource.MustParse("512Mi"))), + vd.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ + URL: object.ImageURLAlpineUEFIPerf, + }), + ) + + // vm-foo always has Main + ClusterNetwork so we can SSH to it. + vmFoo = buildVMWithNetworks("vm-foo", ns, vdFooRoot.Name, vmFooAdditionalIP, true) + vmBar = buildVMWithNetworks("vm-bar", ns, vdBarRoot.Name, vmBarAdditionalIP, tc.vmBarHasMainNetwork) + + err := f.CreateWithDeferredDeletion(context.Background(), vdFooRoot, vdBarRoot, vmFoo, vmBar) + Expect(err).NotTo(HaveOccurred()) + + util.UntilObjectPhase(string(v1alpha2.MachineRunning), framework.LongTimeout, vmFoo, vmBar) + util.UntilVMAgentReady(crclient.ObjectKeyFromObject(vmFoo), framework.LongTimeout) + if tc.vmBarHasMainNetwork { + util.UntilVMAgentReady(crclient.ObjectKeyFromObject(vmBar), framework.LongTimeout) + } + }) + + By("Wait for additional network interfaces to be ready", func() { + util.UntilConditionStatus(vmcondition.TypeNetworkReady.String(), "True", framework.LongTimeout, vmFoo, vmBar) + }) + + By("Check connectivity between VMs via additional network", func() { + checkConnectivityBetweenVMs(f, vmFoo, vmBar) + }) + + By("Create VMOPs to trigger migration", func() { + util.MigrateVirtualMachine(f, vmFoo, vmop.WithGenerateName("vmop-migrate-foo-")) + util.MigrateVirtualMachine(f, vmBar, vmop.WithGenerateName("vmop-migrate-bar-")) + }) + + By("Wait for migration to complete", func() { + util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(vmFoo), framework.LongTimeout) + util.UntilVMMigrationSucceeded(crclient.ObjectKeyFromObject(vmBar), framework.LongTimeout) + }) + + By("Check Cilium agents after migration", func() { + err := network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), vmFoo.Name, f.Namespace().Name) + Expect(err).NotTo(HaveOccurred(), "Cilium agents check for VM %s", vmFoo.Name) + if tc.vmBarHasMainNetwork { + err = network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), vmBar.Name, f.Namespace().Name) + Expect(err).NotTo(HaveOccurred(), "Cilium agents check for VM %s", vmBar.Name) + } + }) + + By("Check VM can reach external network after migration", func() { + if tc.vmBarHasMainNetwork { + network.CheckExternalConnectivity(f, vmFoo.Name, network.ExternalHost, network.HTTPStatusOk) + network.CheckExternalConnectivity(f, vmBar.Name, network.ExternalHost, network.HTTPStatusOk) + } + }) + + By("Wait for additional network interfaces to be ready after migration", func() { + util.UntilConditionStatus(vmcondition.TypeNetworkReady.String(), "True", framework.LongTimeout, vmFoo, vmBar) + }) + + By("Check connectivity between VMs via additional network after migration", func() { + checkConnectivityBetweenVMs(f, vmFoo, vmBar) + }) + }, + Entry("Main + additional network", additionalNetworkTestCase{vmBarHasMainNetwork: true}), + Entry("Only additional network (vm-bar without Main)", additionalNetworkTestCase{vmBarHasMainNetwork: false}), + ) }) +// buildVMWithNetworks creates a VM with optional Main + ClusterNetwork. +// If hasMain is false, only ClusterNetwork is added (VM without Main network). +func buildVMWithNetworks(name, ns, vdRootName, eth1IP string, hasMain bool) *v1alpha2.VirtualMachine { + opts := []vm.Option{ + vm.WithName(name), + vm.WithNamespace(ns), + vm.WithBootloader(v1alpha2.EFI), + vm.WithCPU(1, ptr.To("5%")), + vm.WithMemory(resource.MustParse("256Mi")), + vm.WithRestartApprovalMode(v1alpha2.Manual), + vm.WithVirtualMachineClass(object.DefaultVMClass), + vm.WithLiveMigrationPolicy(v1alpha2.PreferSafeMigrationPolicy), + vm.WithProvisioningUserData(cloudInitAdditionalNetwork(eth1IP)), + vm.WithBlockDeviceRefs(v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.VirtualDiskKind, + Name: vdRootName, + }), + } + if hasMain { + opts = append(opts, + vm.WithNetwork(v1alpha2.NetworksSpec{Type: v1alpha2.NetworksTypeMain}), + ) + } + opts = append(opts, + vm.WithNetwork(v1alpha2.NetworksSpec{ + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: util.ClusterNetworkName, + }), + ) + return vm.New(opts...) +} + // cloudInitAdditionalNetwork returns cloud-init that configures eth1 with the given static IP (Alpine /etc/network/interfaces). func cloudInitAdditionalNetwork(eth1Address string) string { return fmt.Sprintf(`#cloud-config From c127d5533f008f00ea0ae33401338c890b1fb333 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Wed, 18 Feb 2026 17:37:59 +0200 Subject: [PATCH 5/7] fix Signed-off-by: Daniil Antoshin --- test/e2e/vm/additional_network_interfaces.go | 35 ++++++++++++-------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/test/e2e/vm/additional_network_interfaces.go b/test/e2e/vm/additional_network_interfaces.go index 626b4c749d..f21dc43ac9 100644 --- a/test/e2e/vm/additional_network_interfaces.go +++ b/test/e2e/vm/additional_network_interfaces.go @@ -40,7 +40,8 @@ import ( ) const ( - // IPs on additional interface (eth1) for connectivity check between VMs. + // IPs on additional network interface for connectivity check between VMs. + // When VM has Main network, additional interface is eth1; otherwise it's eth0. vmFooAdditionalIP = "192.168.1.10" vmBarAdditionalIP = "192.168.1.11" ) @@ -113,7 +114,7 @@ var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", func() { }) By("Check connectivity between VMs via additional network", func() { - checkConnectivityBetweenVMs(f, vmFoo, vmBar) + checkConnectivityBetweenVMs(f, vmFoo, vmBar, tc.vmBarHasMainNetwork) }) By("Create VMOPs to trigger migration", func() { @@ -147,7 +148,7 @@ var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", func() { }) By("Check connectivity between VMs via additional network after migration", func() { - checkConnectivityBetweenVMs(f, vmFoo, vmBar) + checkConnectivityBetweenVMs(f, vmFoo, vmBar, tc.vmBarHasMainNetwork) }) }, Entry("Main + additional network", additionalNetworkTestCase{vmBarHasMainNetwork: true}), @@ -157,7 +158,8 @@ var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", func() { // buildVMWithNetworks creates a VM with optional Main + ClusterNetwork. // If hasMain is false, only ClusterNetwork is added (VM without Main network). -func buildVMWithNetworks(name, ns, vdRootName, eth1IP string, hasMain bool) *v1alpha2.VirtualMachine { +// The additional network interface is eth1 when hasMain is true, eth0 otherwise. +func buildVMWithNetworks(name, ns, vdRootName, additionalIP string, hasMain bool) *v1alpha2.VirtualMachine { opts := []vm.Option{ vm.WithName(name), vm.WithNamespace(ns), @@ -167,7 +169,7 @@ func buildVMWithNetworks(name, ns, vdRootName, eth1IP string, hasMain bool) *v1a vm.WithRestartApprovalMode(v1alpha2.Manual), vm.WithVirtualMachineClass(object.DefaultVMClass), vm.WithLiveMigrationPolicy(v1alpha2.PreferSafeMigrationPolicy), - vm.WithProvisioningUserData(cloudInitAdditionalNetwork(eth1IP)), + vm.WithProvisioningUserData(cloudInitAdditionalNetwork(additionalIP, hasMain)), vm.WithBlockDeviceRefs(v1alpha2.BlockDeviceSpecRef{ Kind: v1alpha2.VirtualDiskKind, Name: vdRootName, @@ -187,8 +189,13 @@ func buildVMWithNetworks(name, ns, vdRootName, eth1IP string, hasMain bool) *v1a return vm.New(opts...) } -// cloudInitAdditionalNetwork returns cloud-init that configures eth1 with the given static IP (Alpine /etc/network/interfaces). -func cloudInitAdditionalNetwork(eth1Address string) string { +// cloudInitAdditionalNetwork returns cloud-init that configures the additional network interface with the given static IP. +// When hasMain is true, the additional interface is eth1; when false, it's eth0. +func cloudInitAdditionalNetwork(additionalIP string, hasMain bool) string { + ifaceName := "eth0" + if hasMain { + ifaceName = "eth1" + } return fmt.Sprintf(`#cloud-config ssh_pwauth: True users: @@ -207,8 +214,8 @@ write_files: append: true content: | - auto eth1 - iface eth1 inet static + auto %s + iface %s inet static address %s netmask 255.255.255.0 runcmd: @@ -216,10 +223,10 @@ runcmd: - sudo rc-service qemu-guest-agent start - sudo /etc/init.d/networking restart - chown -R cloud:cloud /home/cloud -`, eth1Address) +`, ifaceName, ifaceName, additionalIP) } -func checkConnectivityBetweenVMs(f *framework.Framework, vmFoo, vmBar *v1alpha2.VirtualMachine) { +func checkConnectivityBetweenVMs(f *framework.Framework, vmFoo, vmBar *v1alpha2.VirtualMachine, vmBarHasMainNetwork bool) { GinkgoHelper() pingCmd := "ping -c 2 -W 2 -w 5 -q %s 2>&1 | grep -o \"[0-9]\\+%%\\s*packet loss\"" // %% -> % in output @@ -228,8 +235,10 @@ func checkConnectivityBetweenVMs(f *framework.Framework, vmFoo, vmBar *v1alpha2. By(fmt.Sprintf("VM %s should have connectivity to %s (vm-bar)", vmFoo.Name, vmBarAdditionalIP)) checkResultSSHCommand(f, vmFoo.Name, vmFoo.Namespace, fmt.Sprintf(pingCmd, vmBarAdditionalIP), expectedOut) - By(fmt.Sprintf("VM %s should have connectivity to %s (vm-foo)", vmBar.Name, vmFooAdditionalIP)) - checkResultSSHCommand(f, vmBar.Name, vmBar.Namespace, fmt.Sprintf(pingCmd, vmFooAdditionalIP), expectedOut) + if vmBarHasMainNetwork { + By(fmt.Sprintf("VM %s should have connectivity to %s (vm-foo)", vmBar.Name, vmFooAdditionalIP)) + checkResultSSHCommand(f, vmBar.Name, vmBar.Namespace, fmt.Sprintf(pingCmd, vmFooAdditionalIP), expectedOut) + } } const ( From 7ce8eabe03f76786ab130821d8a7bce5562ce0a3 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Wed, 18 Feb 2026 17:59:39 +0200 Subject: [PATCH 6/7] delete unused Signed-off-by: Daniil Antoshin --- test/e2e/legacy/vm_vpc.go | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 test/e2e/legacy/vm_vpc.go diff --git a/test/e2e/legacy/vm_vpc.go b/test/e2e/legacy/vm_vpc.go deleted file mode 100644 index 6a895b1010..0000000000 --- a/test/e2e/legacy/vm_vpc.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2025 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package legacy - -import ( - "github.com/deckhouse/virtualization/test/e2e/internal/framework" -) - -// isSdnModuleEnabled is used by legacy restore tests (vm_restore_safe, vm_restore_force). -func isSdnModuleEnabled() (bool, error) { - sdnModule, err := framework.NewFramework("").GetModuleConfig("sdn") - if err != nil { - return false, err - } - enabled := sdnModule.Spec.Enabled - - return enabled != nil && *enabled, nil -} From 67e3033cfdfb6a53ee46ee2c15df75ad309af3d8 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Wed, 18 Feb 2026 19:13:06 +0200 Subject: [PATCH 7/7] fix Signed-off-by: Daniil Antoshin --- test/e2e/vm/additional_network_interfaces.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/e2e/vm/additional_network_interfaces.go b/test/e2e/vm/additional_network_interfaces.go index f21dc43ac9..33426f3061 100644 --- a/test/e2e/vm/additional_network_interfaces.go +++ b/test/e2e/vm/additional_network_interfaces.go @@ -130,6 +130,7 @@ var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", func() { By("Check Cilium agents after migration", func() { err := network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), vmFoo.Name, f.Namespace().Name) Expect(err).NotTo(HaveOccurred(), "Cilium agents check for VM %s", vmFoo.Name) + if tc.vmBarHasMainNetwork { err = network.CheckCiliumAgents(context.Background(), f.Clients.Kubectl(), vmBar.Name, f.Namespace().Name) Expect(err).NotTo(HaveOccurred(), "Cilium agents check for VM %s", vmBar.Name) @@ -137,8 +138,9 @@ var _ = Describe("VirtualMachineAdditionalNetworkInterfaces", func() { }) By("Check VM can reach external network after migration", func() { + network.CheckExternalConnectivity(f, vmFoo.Name, network.ExternalHost, network.HTTPStatusOk) + if tc.vmBarHasMainNetwork { - network.CheckExternalConnectivity(f, vmFoo.Name, network.ExternalHost, network.HTTPStatusOk) network.CheckExternalConnectivity(f, vmBar.Name, network.ExternalHost, network.HTTPStatusOk) } })