Skip to content

Commit 6d58ad7

Browse files
authored
fix(vmbda): report error when device is unavailable on VM node (#1561)
Add condition error for PersistentVolume node affinity when attached block devices via VMBDA is not available on VM node. When using storage classes with node affinity, the PersistentVolume is pinned to a specific node. If a user tries to hot-plug such a disk to a VM running on a different node, the attachment fails silently or with unclear errors. Signed-off-by: Daniil Loktev <[email protected]>
1 parent 165ee7d commit 6d58ad7

File tree

3 files changed

+72
-0
lines changed

3 files changed

+72
-0
lines changed

api/core/v1alpha2/vmbdacondition/condition.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ const (
6363
// or the virtual disk is already attached to the virtual machine spec.
6464
// Only the one that was created or started sooner can be processed.
6565
Conflict AttachedReason = "Conflict"
66+
// DeviceNotAvailableOnNode indicates that the block device's PersistentVolume is not available on the node where the virtual machine is running.
67+
DeviceNotAvailableOnNode AttachedReason = "DeviceNotAvailableOnNode"
6668

6769
// CapacityAvailable signifies that the capacity not reached and attaching available.
6870
CapacityAvailable DiskAttachmentCapacityAvailableReason = "CapacityAvailable"

images/virtualization-artifact/pkg/controller/service/attachment_service.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
corev1 "k8s.io/api/core/v1"
2626
"k8s.io/apimachinery/pkg/types"
27+
"k8s.io/component-helpers/scheduling/corev1/nodeaffinity"
2728
virtv1 "kubevirt.io/api/core/v1"
2829
"sigs.k8s.io/controller-runtime/pkg/client"
2930

@@ -250,6 +251,10 @@ func (s AttachmentService) GetPersistentVolumeClaim(ctx context.Context, ad *Att
250251
return object.FetchObject(ctx, types.NamespacedName{Namespace: ad.Namespace, Name: ad.PVCName}, s.client, &corev1.PersistentVolumeClaim{})
251252
}
252253

254+
func (s AttachmentService) GetPersistentVolume(ctx context.Context, pvName string) (*corev1.PersistentVolume, error) {
255+
return object.FetchObject(ctx, types.NamespacedName{Name: pvName}, s.client, &corev1.PersistentVolume{})
256+
}
257+
253258
func (s AttachmentService) GetVirtualMachine(ctx context.Context, name, namespace string) (*v1alpha2.VirtualMachine, error) {
254259
return object.FetchObject(ctx, types.NamespacedName{Namespace: namespace, Name: name}, s.client, &v1alpha2.VirtualMachine{})
255260
}
@@ -262,6 +267,48 @@ func (s AttachmentService) GetKVVMI(ctx context.Context, vm *v1alpha2.VirtualMac
262267
return object.FetchObject(ctx, types.NamespacedName{Namespace: vm.Namespace, Name: vm.Name}, s.client, &virtv1.VirtualMachineInstance{})
263268
}
264269

270+
func (s AttachmentService) IsPVAvailableOnVMNode(ctx context.Context, pvc *corev1.PersistentVolumeClaim, kvvmi *virtv1.VirtualMachineInstance) (bool, error) {
271+
if pvc == nil {
272+
return false, errors.New("pvc is nil")
273+
}
274+
if kvvmi == nil {
275+
return false, errors.New("kvvmi is nil")
276+
}
277+
if pvc.Spec.VolumeName == "" || kvvmi.Status.NodeName == "" {
278+
return true, nil
279+
}
280+
281+
pv, err := s.GetPersistentVolume(ctx, pvc.Spec.VolumeName)
282+
if err != nil {
283+
return false, fmt.Errorf("failed to get PersistentVolume %q: %w", pvc.Spec.VolumeName, err)
284+
}
285+
if pv == nil {
286+
return false, fmt.Errorf("PersistentVolume %q not found", pvc.Spec.VolumeName)
287+
}
288+
289+
if pv.Spec.NodeAffinity == nil || pv.Spec.NodeAffinity.Required == nil {
290+
return true, nil
291+
}
292+
293+
nodeName := kvvmi.Status.NodeName
294+
node := &corev1.Node{}
295+
err = s.client.Get(ctx, types.NamespacedName{Name: nodeName}, node)
296+
if err != nil {
297+
return false, fmt.Errorf("failed to get Node %q: %w", nodeName, err)
298+
}
299+
300+
selector, err := nodeaffinity.NewNodeSelector(pv.Spec.NodeAffinity.Required)
301+
if err != nil {
302+
return false, fmt.Errorf("failed to get node selector: %w", err)
303+
}
304+
305+
if !selector.Match(node) {
306+
return false, nil
307+
}
308+
309+
return true, nil
310+
}
311+
265312
func isSameBlockDeviceRefs(a, b v1alpha2.VMBDAObjectRef) bool {
266313
return a.Kind == b.Kind && a.Name == b.Name
267314
}

images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,29 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vmbda *v1alpha2.VirtualMac
235235
return reconcile.Result{}, nil
236236
}
237237

238+
if ad.PVCName != "" {
239+
pvc, err := h.attacher.GetPersistentVolumeClaim(ctx, ad)
240+
if err != nil {
241+
return reconcile.Result{}, err
242+
}
243+
244+
if pvc != nil {
245+
available, err := h.attacher.IsPVAvailableOnVMNode(ctx, pvc, kvvmi)
246+
if err != nil {
247+
return reconcile.Result{}, err
248+
}
249+
250+
if !available {
251+
vmbda.Status.Phase = v1alpha2.BlockDeviceAttachmentPhaseFailed
252+
cb.
253+
Status(metav1.ConditionFalse).
254+
Reason(vmbdacondition.DeviceNotAvailableOnNode).
255+
Message(fmt.Sprintf("PersistentVolume %q is not available on node %q where the virtual machine is running", pvc.Spec.VolumeName, kvvmi.Status.NodeName))
256+
return reconcile.Result{}, nil
257+
}
258+
}
259+
}
260+
238261
log.Info("Send attachment request")
239262

240263
err = h.attacher.HotPlugDisk(ctx, ad, vm, kvvm)

0 commit comments

Comments
 (0)