diff --git a/pkg/network/vmispec/BUILD.bazel b/pkg/network/vmispec/BUILD.bazel index 149100b92d7f..1230178eb344 100644 --- a/pkg/network/vmispec/BUILD.bazel +++ b/pkg/network/vmispec/BUILD.bazel @@ -26,6 +26,7 @@ go_test( ":go_default_library", "//staging/src/kubevirt.io/api/core/v1:go_default_library", "//staging/src/kubevirt.io/client-go/testutils:go_default_library", + "//tests/libvmi:go_default_library", "//vendor/github.com/onsi/ginkgo/v2:go_default_library", "//vendor/github.com/onsi/gomega:go_default_library", ], diff --git a/pkg/network/vmispec/interface.go b/pkg/network/vmispec/interface.go index 840623c4ed15..e39950adcc5f 100644 --- a/pkg/network/vmispec/interface.go +++ b/pkg/network/vmispec/interface.go @@ -20,6 +20,8 @@ package vmispec import ( + "fmt" + v1 "kubevirt.io/api/core/v1" ) @@ -52,6 +54,36 @@ func FilterInterfacesSpec(ifaces []v1.Interface, predicate func(i v1.Interface) return filteredIfaces } +func VerifyVMIMigratable(vmi *v1.VirtualMachineInstance, bindingPlugins map[string]v1.InterfaceBindingPlugin) error { + ifaces := vmi.Spec.Domain.Devices.Interfaces + if len(ifaces) == 0 { + return nil + } + + _, allowPodBridgeNetworkLiveMigration := vmi.Annotations[v1.AllowPodBridgeNetworkLiveMigrationAnnotation] + if allowPodBridgeNetworkLiveMigration && IsPodNetworkWithBridgeBindingInterface(vmi.Spec.Networks, ifaces) { + return nil + } + if IsPodNetworkWithMasqueradeBindingInterface(vmi.Spec.Networks, ifaces) || IsPodNetworkWithMigratableBindingPlugin(vmi.Spec.Networks, ifaces, bindingPlugins) { + return nil + } + + return fmt.Errorf("cannot migrate VMI which does not use masquerade, bridge with %s VM annotation or a migratable plugin to connect to the pod network", v1.AllowPodBridgeNetworkLiveMigrationAnnotation) + +} + +func IsPodNetworkWithMigratableBindingPlugin(networks []v1.Network, ifaces []v1.Interface, bindingPlugins map[string]v1.InterfaceBindingPlugin) bool { + if podNetwork := LookupPodNetwork(networks); podNetwork != nil { + if podInterface := LookupInterfaceByName(ifaces, podNetwork.Name); podInterface != nil { + if podInterface.Binding != nil { + binding, exist := bindingPlugins[podInterface.Binding.Name] + return exist && binding.Migration != nil + } + } + } + return false +} + func IsPodNetworkWithMasqueradeBindingInterface(networks []v1.Network, ifaces []v1.Interface) bool { if podNetwork := LookupPodNetwork(networks); podNetwork != nil { if podInterface := LookupInterfaceByName(ifaces, podNetwork.Name); podInterface != nil { diff --git a/pkg/network/vmispec/interface_test.go b/pkg/network/vmispec/interface_test.go index c73149f4de2b..13a8b3779d0c 100644 --- a/pkg/network/vmispec/interface_test.go +++ b/pkg/network/vmispec/interface_test.go @@ -22,10 +22,10 @@ package vmispec_test import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - v1 "kubevirt.io/api/core/v1" netvmispec "kubevirt.io/kubevirt/pkg/network/vmispec" + "kubevirt.io/kubevirt/tests/libvmi" ) var _ = Describe("VMI network spec", func() { @@ -126,6 +126,107 @@ var _ = Describe("VMI network spec", func() { Entry("mid", vmiStatusInterfaces(iface1, netName, iface2)), ) }) + + Context("migratable", func() { + const ( + migratablePlugin = "mig" + nonMigratablePlugin = "non_mig" + podNet0 = "default" + ) + + bindingPlugins := map[string]v1.InterfaceBindingPlugin{ + migratablePlugin: {Migration: &v1.InterfaceBindingMigration{}}, + nonMigratablePlugin: {}, + } + + Context("pod network with migratable binding plugin", func() { + It("returns false when there is no pod network", func() { + const nonPodNet = "nonPodNet" + networks := []v1.Network{ + {Name: nonPodNet, NetworkSource: v1.NetworkSource{Multus: &v1.MultusNetwork{}}}, + } + ifaces := []v1.Interface{interfaceWithBridgeBinding(nonPodNet)} + Expect(netvmispec.IsPodNetworkWithMigratableBindingPlugin(networks, ifaces, bindingPlugins)).To(BeFalse()) + }) + It("returns false when the binding is not a plugin", func() { + networks := []v1.Network{podNetwork(podNet0)} + ifaces := []v1.Interface{interfaceWithBridgeBinding(podNet0)} + Expect(netvmispec.IsPodNetworkWithMigratableBindingPlugin(networks, ifaces, bindingPlugins)).To(BeFalse()) + }) + + It("returns false when the plugin is not migratable", func() { + networks := []v1.Network{podNetwork(podNet0)} + ifaces := []v1.Interface{interfaceWithBindingPlugin(podNet0, nonMigratablePlugin)} + Expect(netvmispec.IsPodNetworkWithMigratableBindingPlugin(networks, ifaces, bindingPlugins)).To(BeFalse()) + }) + + It("returns false when non pod network is migratable", func() { + const nonPodNetName = "nonPod" + nonPodNetwork := v1.Network{ + Name: nonPodNetName, + NetworkSource: v1.NetworkSource{Multus: &v1.MultusNetwork{}}, + } + networks := []v1.Network{nonPodNetwork} + ifaces := []v1.Interface{interfaceWithBindingPlugin(nonPodNetName, migratablePlugin)} + Expect(netvmispec.IsPodNetworkWithMigratableBindingPlugin(networks, ifaces, bindingPlugins)).To(BeFalse()) + }) + + It("returns true when the plugin is migratable", func() { + networks := []v1.Network{podNetwork(podNet0)} + ifaces := []v1.Interface{interfaceWithBindingPlugin(podNet0, migratablePlugin)} + Expect(netvmispec.IsPodNetworkWithMigratableBindingPlugin(networks, ifaces, bindingPlugins)).To(BeTrue()) + }) + It("returns true when the secondary interface has is migratable", func() { + networks := []v1.Network{podNetwork(podNet0)} + ifaces := []v1.Interface{interfaceWithBindingPlugin(podNet0, migratablePlugin)} + Expect(netvmispec.IsPodNetworkWithMigratableBindingPlugin(networks, ifaces, bindingPlugins)).To(BeTrue()) + }) + }) + + Context("vmi", func() { + It("shouldn't allow migration if the VMI use non-migratable binding plugin to connect to the pod network", func() { + network := podNetwork(podNet0) + vmi := libvmi.New( + libvmi.WithInterface(interfaceWithBindingPlugin(podNet0, nonMigratablePlugin)), + libvmi.WithNetwork(&network), + ) + Expect(netvmispec.VerifyVMIMigratable(vmi, bindingPlugins)).ToNot(Succeed()) + }) + It("shouldn't allow migration if the VMI uses bridge binding to connect to the pod network", func() { + network := podNetwork(podNet0) + vmi := libvmi.New( + libvmi.WithInterface(*v1.DefaultBridgeNetworkInterface()), + libvmi.WithNetwork(&network), + ) + Expect(netvmispec.VerifyVMIMigratable(vmi, bindingPlugins)).ToNot(Succeed()) + }) + It("should allow migration if the VMI uses masquerade to connect to the pod network", func() { + network := podNetwork(podNet0) + vmi := libvmi.New( + libvmi.WithInterface(*v1.DefaultMasqueradeNetworkInterface()), + libvmi.WithNetwork(&network), + ) + Expect(netvmispec.VerifyVMIMigratable(vmi, bindingPlugins)).To(Succeed()) + }) + It("should allow migration if the VMI use bridge to connect to the pod network and has AllowLiveMigrationBridgePodNetwork annotation", func() { + network := podNetwork(podNet0) + vmi := libvmi.New( + libvmi.WithInterface(*v1.DefaultBridgeNetworkInterface()), + libvmi.WithNetwork(&network), + libvmi.WithAnnotation(v1.AllowPodBridgeNetworkLiveMigrationAnnotation, ""), + ) + Expect(netvmispec.VerifyVMIMigratable(vmi, bindingPlugins)).To(Succeed()) + }) + It("should allow migration if the VMI use migratable binding plugin to connect to the pod network", func() { + network := podNetwork(podNet0) + vmi := libvmi.New( + libvmi.WithInterface(interfaceWithBindingPlugin(podNet0, migratablePlugin)), + libvmi.WithNetwork(&network), + ) + Expect(netvmispec.VerifyVMIMigratable(vmi, bindingPlugins)).To(Succeed()) + }) + }) + }) }) func podNetwork(name string) v1.Network { @@ -149,6 +250,13 @@ func interfaceWithMasqueradeBinding(name string) v1.Interface { } } +func interfaceWithBindingPlugin(name, pluginName string) v1.Interface { + return v1.Interface{ + Name: name, + Binding: &v1.PluginBinding{Name: pluginName}, + } +} + func vmiStatusInterfaces(names ...string) []v1.VirtualMachineInstanceNetworkInterface { var statusInterfaces []v1.VirtualMachineInstanceNetworkInterface for _, name := range names { diff --git a/pkg/virt-handler/vm.go b/pkg/virt-handler/vm.go index 803352070387..afdac5d0b3ea 100644 --- a/pkg/virt-handler/vm.go +++ b/pkg/virt-handler/vm.go @@ -2452,20 +2452,7 @@ func (d *VirtualMachineController) isPreMigrationTarget(vmi *v1.VirtualMachineIn } func (d *VirtualMachineController) checkNetworkInterfacesForMigration(vmi *v1.VirtualMachineInstance) error { - ifaces := vmi.Spec.Domain.Devices.Interfaces - if len(ifaces) == 0 { - return nil - } - - _, allowPodBridgeNetworkLiveMigration := vmi.Annotations[v1.AllowPodBridgeNetworkLiveMigrationAnnotation] - if allowPodBridgeNetworkLiveMigration && netvmispec.IsPodNetworkWithBridgeBindingInterface(vmi.Spec.Networks, ifaces) { - return nil - } - if netvmispec.IsPodNetworkWithMasqueradeBindingInterface(vmi.Spec.Networks, ifaces) { - return nil - } - - return fmt.Errorf("cannot migrate VMI which does not use masquerade to connect to the pod network or bridge with %s VM annotation", v1.AllowPodBridgeNetworkLiveMigrationAnnotation) + return netvmispec.VerifyVMIMigratable(vmi, d.clusterConfig.GetNetworkBindings()) } func (d *VirtualMachineController) checkVolumesForMigration(vmi *v1.VirtualMachineInstance) (blockMigrate bool, err error) { diff --git a/pkg/virt-handler/vm_test.go b/pkg/virt-handler/vm_test.go index 98ff84d49a01..8574f0c02c32 100644 --- a/pkg/virt-handler/vm_test.go +++ b/pkg/virt-handler/vm_test.go @@ -127,6 +127,8 @@ var _ = Describe("VirtualMachineInstance", func() { var certDir string + const migratableNetworkBindingPlugin = "mig_plug" + getCgroupManager = func(_ *v1.VirtualMachineInstance) (cgroup.Manager, error) { return mockCgroupManager, nil } @@ -186,7 +188,11 @@ var _ = Describe("VirtualMachineInstance", func() { vmiInterface = kubecli.NewMockVirtualMachineInstanceInterface(ctrl) virtClient.EXPECT().VirtualMachineInstance(metav1.NamespaceDefault).Return(vmiInterface).AnyTimes() mockWatchdog = &MockWatchdog{shareDir} - config, _, _ := testutils.NewFakeClusterConfigUsingKVConfig(&v1.KubeVirtConfiguration{}) + kv := &v1.KubeVirtConfiguration{} + kv.NetworkConfiguration = &v1.NetworkConfiguration{Binding: map[string]v1.InterfaceBindingPlugin{ + migratableNetworkBindingPlugin: {Migration: &v1.InterfaceBindingMigration{}}, + }} + config, _, _ := testutils.NewFakeClusterConfigUsingKVConfig(kv) Expect(os.MkdirAll(filepath.Join(vmiShareDir, "dev"), 0755)).To(Succeed()) f, err := os.OpenFile(filepath.Join(vmiShareDir, "dev", "kvm"), os.O_CREATE, 0755) @@ -2687,7 +2693,7 @@ var _ = Describe("VirtualMachineInstance", func() { Expect(condition.Reason).To(Equal(v1.VirtualMachineInstanceReasonVirtIOFSNotMigratable)) }) - It("should not be allowed to live-migrate if the VMI does not use masquerade to connect to the pod network", func() { + It("should not be allowed to live-migrate if the VMI has non-migratable interface", func() { vmi := api2.NewMinimalVMI("testvmi") strategy := v1.EvictionStrategyLiveMigrate @@ -2699,28 +2705,7 @@ var _ = Describe("VirtualMachineInstance", func() { conditionManager := virtcontroller.NewVirtualMachineInstanceConditionManager() controller.updateLiveMigrationConditions(vmi, conditionManager) - testutils.ExpectEvent(recorder, fmt.Sprintf("cannot migrate VMI which does not use masquerade to connect to the pod network or bridge with %s VM annotation", v1.AllowPodBridgeNetworkLiveMigrationAnnotation)) - }) - Context("with AllowLiveMigrationBridgePodNetwork annotation", func() { - It("should allow to live-migrate if the VMI use bridge to connect to the pod network", func() { - vmi := api2.NewMinimalVMI("testvmi") - - vmi.Annotations = map[string]string{v1.AllowPodBridgeNetworkLiveMigrationAnnotation: ""} - - strategy := v1.EvictionStrategyLiveMigrate - vmi.Spec.EvictionStrategy = &strategy - - vmi.Spec.Domain.Devices.Interfaces = []v1.Interface{*v1.DefaultBridgeNetworkInterface()} - vmi.Spec.Networks = []v1.Network{*v1.DefaultPodNetwork()} - - conditionManager := virtcontroller.NewVirtualMachineInstanceConditionManager() - controller.updateLiveMigrationConditions(vmi, conditionManager) - Expect(vmi.Status.Conditions).To(ContainElement(v1.VirtualMachineInstanceCondition{ - Type: v1.VirtualMachineInstanceIsMigratable, - Status: k8sv1.ConditionTrue, - })) - Expect(vmi.Status.MigrationMethod).To(Equal(v1.LiveMigration)) - }) + testutils.ExpectEvent(recorder, fmt.Sprintf("cannot migrate VMI which does not use masquerade, bridge with %s VM annotation or a migratable plugin to connect to the pod network", v1.AllowPodBridgeNetworkLiveMigrationAnnotation)) }) Context("check that migration is not supported when using Host Devices", func() {