diff --git a/api/core/v1alpha2/vmbdacondition/condition.go b/api/core/v1alpha2/vmbdacondition/condition.go index 19d30e18cd..23500d3c5c 100644 --- a/api/core/v1alpha2/vmbdacondition/condition.go +++ b/api/core/v1alpha2/vmbdacondition/condition.go @@ -65,6 +65,8 @@ const ( Conflict AttachedReason = "Conflict" // DeviceNotAvailableOnNode indicates that the block device's PersistentVolume is not available on the node where the virtual machine is running. DeviceNotAvailableOnNode AttachedReason = "DeviceNotAvailableOnNode" + // BlockedByMigration indicates that a block device cannot be hot-plugged while the virtual machine is migrating. + BlockedByMigration AttachedReason = "BlockedByMigration" // CapacityAvailable signifies that the capacity not reached and attaching available. CapacityAvailable DiskAttachmentCapacityAvailableReason = "CapacityAvailable" diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go index 6dfce1a459..105aa02728 100644 --- a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go @@ -31,6 +31,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/logger" "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vmbdacondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" ) type LifeCycleHandler struct { @@ -247,6 +248,17 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vmbda *v1alpha2.VirtualMac } if ad.PVCName != "" { + if isVirtualMachineMigrating(vm) { + log.Info("Cannot hotplug a disk while the virtual machine is migrating") + + vmbda.Status.Phase = v1alpha2.BlockDeviceAttachmentPhasePending + cb. + Status(metav1.ConditionFalse). + Reason(vmbdacondition.BlockedByMigration). + Message("Cannot hotplug a disk while the virtual machine is migrating.") + return reconcile.Result{}, nil + } + pvc, err := h.attacher.GetPersistentVolumeClaim(ctx, ad) if err != nil { return reconcile.Result{}, err @@ -301,3 +313,8 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vmbda *v1alpha2.VirtualMac return reconcile.Result{}, canErr } } + +func isVirtualMachineMigrating(vm *v1alpha2.VirtualMachine) bool { + _, migrating := conditions.GetCondition(vmcondition.TypeMigrating, vm.Status.Conditions) + return migrating +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go new file mode 100644 index 0000000000..5a3c381da0 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle_test.go @@ -0,0 +1,46 @@ +/* +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 ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +var _ = Describe("LifeCycleHandler helpers", func() { + DescribeTable("isVirtualMachineMigrating", func(status metav1.ConditionStatus, hasCondition, expected bool) { + vm := &v1alpha2.VirtualMachine{} + if hasCondition { + vm.Status.Conditions = []metav1.Condition{ + { + Type: vmcondition.TypeMigrating.String(), + Status: status, + }, + } + } + Expect(isVirtualMachineMigrating(vm)).To(Equal(expected)) + }, + Entry("migrating condition is true", metav1.ConditionTrue, true, true), + Entry("migrating condition is false", metav1.ConditionFalse, true, true), + Entry("migrating condition is unknown", metav1.ConditionUnknown, true, true), + Entry("migrating condition is absent", metav1.ConditionTrue, false, false), + ) +}) diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go index efca5f6e84..a08c556e8b 100644 --- a/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go @@ -61,6 +61,12 @@ func (w VirtualMachineWatcher) Watch(mgr manager.Manager, ctr controller.Control return true } + _, oldMigrating := conditions.GetCondition(vmcondition.TypeMigrating, e.ObjectOld.Status.Conditions) + _, newMigrating := conditions.GetCondition(vmcondition.TypeMigrating, e.ObjectNew.Status.Conditions) + if oldMigrating != newMigrating { + return true + } + return w.hasBlockDeviceAttachmentChanges(e.ObjectOld, e.ObjectNew) }, },