diff --git a/api/core/v1alpha2/vmcondition/condition.go b/api/core/v1alpha2/vmcondition/condition.go index fa4b44209d..6abfecbe0c 100644 --- a/api/core/v1alpha2/vmcondition/condition.go +++ b/api/core/v1alpha2/vmcondition/condition.go @@ -258,7 +258,6 @@ const ( ReasonDisksNotMigratable MigratableReason = "VirtualMachineDisksNotMigratable" ReasonDisksShouldBeMigrating MigratableReason = "VirtualMachineDisksShouldBeMigrating" ReasonHostDevicesNotMigratable MigratableReason = "VirtualMachineHostDevicesNotMigratable" - ReasonUSBShouldBeMigrating MigratableReason = "VirtualMachineUSBShouldBeMigrating" ) type MigratingReason string diff --git a/build/components/versions.yml b/build/components/versions.yml index 029a9d6c95..beb156da4b 100644 --- a/build/components/versions.yml +++ b/build/components/versions.yml @@ -3,7 +3,7 @@ firmware: libvirt: v10.9.0 edk2: stable202411 core: - 3p-kubevirt: v1.6.2-v12n.15 + 3p-kubevirt: v1.6.2-v12n.16 3p-containerized-data-importer: v1.60.3-v12n.16 distribution: 2.8.3 package: diff --git a/crds/embedded/virtualmachineinstances.yaml b/crds/embedded/virtualmachineinstances.yaml index 4b4c59adba..c94463fec5 100644 --- a/crds/embedded/virtualmachineinstances.yaml +++ b/crds/embedded/virtualmachineinstances.yaml @@ -3585,6 +3585,10 @@ spec: deviceResourceClaimStatus: description: DRA-related information for the device. properties: + allowMultipleAllocations: + description: AllowMultipleAllocations is a flag to allow multiple + allocations of the same device + type: boolean attributes: description: |- Properties of the device that could be used by kubevirt and other components to learn more @@ -3597,6 +3601,10 @@ spec: description: PCIe bus address of the allocated device. type: string type: object + bindsToNode: + description: BindsToNode is a flag to bind the device to the + node + type: boolean name: description: Name of actual device on the host provisioned by the driver as reflected in `resourceclaim.status`. type: string @@ -3633,6 +3641,10 @@ spec: deviceResourceClaimStatus: description: DRA-related information for the device. properties: + allowMultipleAllocations: + description: AllowMultipleAllocations is a flag to allow multiple + allocations of the same device + type: boolean attributes: description: |- Properties of the device that could be used by kubevirt and other components to learn more @@ -3661,6 +3673,10 @@ spec: - deviceNumber type: object type: object + bindsToNode: + description: BindsToNode is a flag to bind the device to the + node + type: boolean name: description: Name of actual device on the host provisioned by the driver as reflected in `resourceclaim.status`. type: string diff --git a/images/virtualization-artifact/go.mod b/images/virtualization-artifact/go.mod index 41b72d703a..5f55b00818 100644 --- a/images/virtualization-artifact/go.mod +++ b/images/virtualization-artifact/go.mod @@ -168,4 +168,4 @@ replace ( ) // Kubevirt API replaces -replace kubevirt.io/api => github.com/deckhouse/3p-kubevirt/staging/src/kubevirt.io/api v1.6.2-v12n.12 +replace kubevirt.io/api => github.com/deckhouse/3p-kubevirt/staging/src/kubevirt.io/api v1.6.2-v12n.16 diff --git a/images/virtualization-artifact/go.sum b/images/virtualization-artifact/go.sum index f296612ef5..1b4103f2be 100644 --- a/images/virtualization-artifact/go.sum +++ b/images/virtualization-artifact/go.sum @@ -49,8 +49,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckhouse/3p-kubevirt/staging/src/kubevirt.io/api v1.6.2-v12n.12 h1:KJoeBr01U8q2NXKvx4iCQwW8K6voiUrI1oOzDuaEvSk= -github.com/deckhouse/3p-kubevirt/staging/src/kubevirt.io/api v1.6.2-v12n.12/go.mod h1:J2HiSOLVhKj2UXoe14V6rpr3ZO/W/QBxcN+QK/S0Xkc= +github.com/deckhouse/3p-kubevirt/staging/src/kubevirt.io/api v1.6.2-v12n.16 h1:HzyValkO6ODGNvGtHhu11R7QkXzNkk4vmPgFouLE3NM= +github.com/deckhouse/3p-kubevirt/staging/src/kubevirt.io/api v1.6.2-v12n.16/go.mod h1:J2HiSOLVhKj2UXoe14V6rpr3ZO/W/QBxcN+QK/S0Xkc= github.com/deckhouse/deckhouse/pkg/log v0.0.0-20250226105106-176cd3afcdd5 h1:PsN1E0oxC/+4zdA977txrqUCuObFL3HAuu5Xnud8m8c= github.com/deckhouse/deckhouse/pkg/log v0.0.0-20250226105106-176cd3afcdd5/go.mod h1:Mk5HRzkc5pIcDIZ2JJ6DPuuqnwhXVkb3you8M8Mg+4w= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go index 6c763eb6ef..33ce9d7e02 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go @@ -655,3 +655,7 @@ func (b *KVVM) SetMetadata(metadata metav1.ObjectMeta) { func (b *KVVM) SetUpdateVolumesStrategy(strategy *virtv1.UpdateVolumesStrategy) { b.Resource.Spec.UpdateVolumesStrategy = strategy } + +func (b *KVVM) SetUSBMigrationStrategy() { + b.SetKVVMIAnnotation(virtv1.USBMigrationStrategyAnn, string(virtv1.USBMigrationStrategyIgnore)) +} diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go index a745c1021e..8e09166b0d 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go @@ -109,6 +109,7 @@ func ApplyVirtualMachineSpec( return err } + kvvm.SetUSBMigrationStrategy() kvvm.SetMetadata(vm.ObjectMeta) setNetwork(kvvm, networkSpec) kvvm.SetTablet("default-0") diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/migrating.go b/images/virtualization-artifact/pkg/controller/vm/internal/migrating.go index 2c52e6cbb7..b7a4ba143a 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/migrating.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/migrating.go @@ -78,7 +78,7 @@ func (h *MigratingHandler) Handle(ctx context.Context, s state.VirtualMachineSta vm.Status.MigrationState = migrationState } - err = h.syncMigratable(ctx, s, vm, kvvm, kvvmi) + err = h.syncMigratable(ctx, s, vm, kvvm) if err != nil { return reconcile.Result{}, fmt.Errorf("failed to sync migratable condition: %w", err) } @@ -277,7 +277,7 @@ func (h *MigratingHandler) getVMOPCandidate(ctx context.Context, s state.Virtual return nil, nil } -func (h *MigratingHandler) syncMigratable(ctx context.Context, s state.VirtualMachineState, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance) error { +func (h *MigratingHandler) syncMigratable(ctx context.Context, s state.VirtualMachineState, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine) error { cb := conditions.NewConditionBuilder(vmcondition.TypeMigratable).Generation(vm.GetGeneration()) if kvvm != nil { @@ -297,18 +297,9 @@ func (h *MigratingHandler) syncMigratable(ctx context.Context, s state.VirtualMa conditions.SetCondition(cb, &vm.Status.Conditions) return nil case liveMigratable.Reason == virtv1.VirtualMachineInstanceReasonHostDeviceNotMigratable: - hostDeviceNames := getHostDeviceNamesFromKVVMI(kvvmi) - isAllUSB := allHostDevicesAreUSB(hostDeviceNames) - - if isAllUSB { - cb.Status(metav1.ConditionTrue). - Reason(vmcondition.ReasonUSBShouldBeMigrating). - Message("") - } else { - cb.Status(metav1.ConditionFalse). - Reason(vmcondition.ReasonHostDevicesNotMigratable). - Message("Live migration requires that all Host Devices must be unplugged") - } + cb.Status(metav1.ConditionFalse). + Reason(vmcondition.ReasonHostDevicesNotMigratable). + Message("Live migration requires that all Host Devices must be unplugged") conditions.SetCondition(cb, &vm.Status.Conditions) return nil @@ -354,43 +345,3 @@ func (h *MigratingHandler) syncMigratable(ctx context.Context, s state.VirtualMa func liveMigrationInProgress(migrationState *v1alpha2.VirtualMachineMigrationState) bool { return migrationState != nil && migrationState.StartTimestamp != nil && migrationState.EndTimestamp == nil } - -// getHostDeviceNamesFromKVVMI returns unique host device names from both Spec and Status. -// Either spec or status (or both) can be nil when the other has devices, e.g. after deleting hp pod. -func getHostDeviceNamesFromKVVMI(kvvmi *virtv1.VirtualMachineInstance) []string { - if kvvmi == nil { - return nil - } - names := make(map[string]struct{}) - if kvvmi.Spec.Domain.Devices.HostDevices != nil { - for _, d := range kvvmi.Spec.Domain.Devices.HostDevices { - if d.Name != "" { - names[d.Name] = struct{}{} - } - } - } - if kvvmi.Status.DeviceStatus != nil && kvvmi.Status.DeviceStatus.HostDeviceStatuses != nil { - for _, st := range kvvmi.Status.DeviceStatus.HostDeviceStatuses { - if st.Name != "" { - names[st.Name] = struct{}{} - } - } - } - result := make([]string, 0, len(names)) - for n := range names { - result = append(result, n) - } - return result -} - -func allHostDevicesAreUSB(deviceNames []string) bool { - if len(deviceNames) == 0 { - return true // no host devices visible — do not block migration (e.g. when spec/status incomplete) - } - for _, name := range deviceNames { - if !strings.HasPrefix(name, "usb-") { - return false - } - } - return true -} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_metadata.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_metadata.go index 8042091cb8..df72bb67d9 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_metadata.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_metadata.go @@ -212,6 +212,10 @@ func (h *SyncMetadataHandler) updateKVVMSpecTemplateMetadataAnnotations(currAnno res[netmanager.AnnoIPAddressCNIRequest] = v } + if v, ok := currAnno[virtv1.USBMigrationStrategyAnn]; ok { + res[virtv1.USBMigrationStrategyAnn] = v + } + return commonvm.RemoveNonPropagatableAnnotations(res) } diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/usb_device_migration_handler.go b/images/virtualization-artifact/pkg/controller/vm/internal/usb_device_migration_handler.go deleted file mode 100644 index 471c0d1981..0000000000 --- a/images/virtualization-artifact/pkg/controller/vm/internal/usb_device_migration_handler.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2026 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 internal - -import ( - "context" - "fmt" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" -) - -const nameUSBDeviceMigrationHandler = "USBDeviceMigrationHandler" - -// USBDeviceMigrationHandler unplugs all attached USB devices when migration is pending. -// After migration completes, the normal attach handler will plug them back (DRA over network). -func NewUSBDeviceMigrationHandler(cl client.Client, virtClient VirtClient) *USBDeviceMigrationHandler { - return &USBDeviceMigrationHandler{ - usbDeviceHandlerBase: usbDeviceHandlerBase{ - client: cl, - virtClient: virtClient, - }, - } -} - -type USBDeviceMigrationHandler struct { - usbDeviceHandlerBase -} - -func (h *USBDeviceMigrationHandler) Name() string { - return nameUSBDeviceMigrationHandler -} - -func (h *USBDeviceMigrationHandler) Handle(ctx context.Context, s state.VirtualMachineState) (reconcile.Result, error) { - if s.VirtualMachine().IsEmpty() { - return reconcile.Result{}, nil - } - - vm := s.VirtualMachine().Current() - changed := s.VirtualMachine().Changed() - - migratingCond, exists := conditions.GetCondition(vmcondition.TypeMigratable, changed.Status.Conditions) - if !exists { - return reconcile.Result{}, nil - } - - if migratingCond.Reason != vmcondition.ReasonUSBShouldBeMigrating.String() { - return reconcile.Result{}, nil - } - - hasPendingMigration, err := h.hasPendingMigrationOp(ctx, s) - if err != nil { - return reconcile.Result{}, err - } - - if !hasPendingMigration { - return reconcile.Result{}, nil - } - - for i := 0; i < len(changed.Status.USBDevices); i++ { - ref := &changed.Status.USBDevices[i] - err := h.detachUSBDevice(ctx, vm, ref.Name) - if err != nil && !apierrors.IsNotFound(err) { - return reconcile.Result{}, fmt.Errorf("failed to unplug USB device for migration %s: %w", ref.Name, err) - } - - ref.Attached = false - ref.Address = nil - ref.Hotplugged = false - } - - return reconcile.Result{}, nil -} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/usb_device_migration_handler_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/usb_device_migration_handler_test.go deleted file mode 100644 index e3374273d8..0000000000 --- a/images/virtualization-artifact/pkg/controller/vm/internal/usb_device_migration_handler_test.go +++ /dev/null @@ -1,151 +0,0 @@ -/* -Copyright 2026 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 internal - -import ( - "context" - "errors" - "log/slog" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" - "github.com/deckhouse/virtualization-controller/pkg/logger" - "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" -) - -var _ = Describe("USBDeviceMigrationHandler", func() { - var ctx context.Context - var mockVirtCl *mockVirtClient - var handler *USBDeviceMigrationHandler - var vmState state.VirtualMachineState - - BeforeEach(func() { - ctx = logger.ToContext(context.TODO(), slog.Default()) - mockVirtCl = newMockVirtClient() - }) - - DescribeTable("Handle migration matrix", - func(reason string, vmopPhase v1alpha2.VMOPPhase, expectDetach bool) { - vm := &v1alpha2.VirtualMachine{ - ObjectMeta: metav1.ObjectMeta{Name: "test-vm", Namespace: "default", UID: types.UID("vm-uid")}, - Status: v1alpha2.VirtualMachineStatus{ - USBDevices: []v1alpha2.USBDeviceStatusRef{{Name: "usb-device-1", Attached: true}}, - Conditions: []metav1.Condition{{ - Type: string(vmcondition.TypeMigratable), - Status: metav1.ConditionFalse, - Reason: reason, - }}, - }, - } - - objs := []client.Object{} - if vmopPhase != "" { - objs = append(objs, &v1alpha2.VirtualMachineOperation{ - ObjectMeta: metav1.ObjectMeta{Name: "vmop-1", Namespace: "default"}, - Spec: v1alpha2.VirtualMachineOperationSpec{VirtualMachine: "test-vm", Type: v1alpha2.VMOPTypeMigrate}, - Status: v1alpha2.VirtualMachineOperationStatus{Phase: vmopPhase}, - }) - } - - fakeClient, _, st := setupEnvironment(vm, objs...) - vmState = st - handler = NewUSBDeviceMigrationHandler(fakeClient, mockVirtCl) - - _, err := handler.Handle(ctx, vmState) - Expect(err).NotTo(HaveOccurred()) - - _ = mockVirtCl.VirtualMachines("default") - calls := len(mockVirtCl.vmClients["default"].removeResourceClaimCalls) - if expectDetach { - Expect(calls).To(Equal(1)) - } else { - Expect(calls).To(Equal(0)) - } - }, - Entry("usb migration reason + pending vmop", vmcondition.ReasonUSBShouldBeMigrating.String(), v1alpha2.VMOPPhasePending, true), - Entry("usb migration reason + inprogress vmop", vmcondition.ReasonUSBShouldBeMigrating.String(), v1alpha2.VMOPPhaseInProgress, false), - Entry("other migratable reason", vmcondition.ReasonMigratable.String(), v1alpha2.VMOPPhasePending, false), - ) - - It("should detach all USB devices when migration is pending", func() { - vm := &v1alpha2.VirtualMachine{ - ObjectMeta: metav1.ObjectMeta{Name: "test-vm", Namespace: "default", UID: types.UID("vm-uid")}, - Status: v1alpha2.VirtualMachineStatus{ - USBDevices: []v1alpha2.USBDeviceStatusRef{{Name: "usb-device-1", Attached: true, Hotplugged: true}, {Name: "usb-device-2", Attached: true}}, - Conditions: []metav1.Condition{{ - Type: string(vmcondition.TypeMigratable), - Status: metav1.ConditionFalse, - Reason: vmcondition.ReasonUSBShouldBeMigrating.String(), - }}, - }, - } - vmop := &v1alpha2.VirtualMachineOperation{ - ObjectMeta: metav1.ObjectMeta{Name: "vmop-1", Namespace: "default"}, - Spec: v1alpha2.VirtualMachineOperationSpec{VirtualMachine: "test-vm", Type: v1alpha2.VMOPTypeMigrate}, - Status: v1alpha2.VirtualMachineOperationStatus{Phase: v1alpha2.VMOPPhasePending}, - } - - fakeClient, vmResource, st := setupEnvironment(vm, vmop) - vmState = st - handler = NewUSBDeviceMigrationHandler(fakeClient, mockVirtCl) - - result, err := handler.Handle(ctx, vmState) - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(reconcile.Result{})) - - mockVM := mockVirtCl.vmClients["default"] - Expect(mockVM.removeResourceClaimCalls).To(HaveLen(2)) - Expect(vmResource.Changed().Status.USBDevices[0].Attached).To(BeFalse()) - Expect(vmResource.Changed().Status.USBDevices[0].Hotplugged).To(BeFalse()) - }) - - It("should return error when detach fails during migration", func() { - vm := &v1alpha2.VirtualMachine{ - ObjectMeta: metav1.ObjectMeta{Name: "test-vm", Namespace: "default", UID: types.UID("vm-uid")}, - Status: v1alpha2.VirtualMachineStatus{ - USBDevices: []v1alpha2.USBDeviceStatusRef{{Name: "usb-device-1", Attached: true}}, - Conditions: []metav1.Condition{{ - Type: string(vmcondition.TypeMigratable), - Status: metav1.ConditionFalse, - Reason: vmcondition.ReasonUSBShouldBeMigrating.String(), - }}, - }, - } - vmop := &v1alpha2.VirtualMachineOperation{ - ObjectMeta: metav1.ObjectMeta{Name: "vmop-1", Namespace: "default"}, - Spec: v1alpha2.VirtualMachineOperationSpec{VirtualMachine: "test-vm", Type: v1alpha2.VMOPTypeMigrate}, - Status: v1alpha2.VirtualMachineOperationStatus{Phase: v1alpha2.VMOPPhasePending}, - } - - fakeClient, _, st := setupEnvironment(vm, vmop) - vmState = st - handler = NewUSBDeviceMigrationHandler(fakeClient, mockVirtCl) - - mockVM := mockVirtCl.VirtualMachines("default").(*mockVirtualMachines) - mockVM.removeResourceClaimErr = errors.New("boom") - - _, err := handler.Handle(ctx, vmState) - Expect(err).To(HaveOccurred()) - }) -}) diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_controller.go b/images/virtualization-artifact/pkg/controller/vm/vm_controller.go index dd22ee3236..6a4e231a88 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_controller.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_controller.go @@ -84,7 +84,6 @@ func SetupController( internal.NewFirmwareHandler(firmwareImage), internal.NewEvictHandler(), internal.NewStatisticHandler(client), - internal.NewUSBDeviceMigrationHandler(client, virtClient), } r := NewReconciler(client, handlers...)