Skip to content

fix(csi): detach orphan hot-plug volumes when VMI outlives its VM - #2866

Merged
Andrei Kvapil (kvaps) merged 2 commits into
mainfrom
fix/kubevirt-csi-orphan-detach
Jun 22, 2026
Merged

fix(csi): detach orphan hot-plug volumes when VMI outlives its VM#2866
Andrei Kvapil (kvaps) merged 2 commits into
mainfrom
fix/kubevirt-csi-orphan-detach

Conversation

@kvaps

@kvaps Andrei Kvapil (kvaps) commented Jun 10, 2026

Copy link
Copy Markdown
Member

What this PR does

During node reprovisioning in tenant Kubernetes clusters, the VMI (and its hot-plug pod) can outlive the parent VirtualMachine. The upstream kubevirt-csi-driver treats a missing VM as "detach succeeded", so the hot-plug pod stays alive and keeps the infra storage device exclusively attached to the source host — every subsequent attach of that volume fails, on DRBD with failed to set source device readwrite. This was hit repeatedly in production tenant clusters (see kubevirt/csi-driver#182).

This PR:

  • updates the pinned kubevirt.io/csi-driver module to the latest upstream commit;
  • backports Check VMI status when detaching, not just VM spec kubevirt/csi-driver#184 into the controller wrapper: after the upstream unpublish returns success, the wrapper re-checks VMI.status.volumeStatus and, if the hot-plug is still reported there (e.g. because the parent VM is already gone), detaches it from the VMI directly via the removevolume subresource and waits until it disappears.

The backport is self-contained in the wrapper and can be dropped once kubevirt/csi-driver#184 merges upstream and the module pin is bumped past it.

Credit to mattia-eleuteri for the root-cause analysis and the upstream fix.

Screenshots

Not a UI change.

Release note

fix(csi): tenant CSI driver no longer leaves orphan hot-plug volumes exclusively attached to the source host when the VMI outlives its deleted VirtualMachine during node reprovisioning

Summary by CodeRabbit

  • New Features
    • Enhanced hotplug volume unpublishing with automatic verification to ensure volumes are fully detached before completion, including retry logic with up to 2-minute timeout.
  • Dependencies
    • Updated KubeVirt API and CSI driver dependencies to latest versions.

Andrei Kvapil (kvaps) and others added 2 commits June 10, 2026 11:30
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Upstream treats a missing parent VM as detach success, so when the VMI
outlives its VM during node reprovisioning the hot-plug pod keeps the
infra device exclusively attached to the source host and subsequent
attaches fail. Re-check VMI volume status after upstream unpublish and
detach from the VMI directly. Backport of kubevirt/csi-driver#184.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
@kvaps Andrei Kvapil (kvaps) added the debug Debugging in progress label Jun 10, 2026
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff89fc04-72af-4b5e-a28c-c806f8aced07

📥 Commits

Reviewing files that changed from the base of the PR and between c6f6fee and 68136f9.

⛔ Files ignored due to path filters (1)
  • packages/apps/kubernetes/images/kubevirt-csi-driver/go.sum is excluded by !**/*.sum
📒 Files selected for processing (2)
  • packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go
  • packages/apps/kubernetes/images/kubevirt-csi-driver/go.mod

📝 Walkthrough

Walkthrough

WrappedControllerService adds post-unpublish verification for hotplug volumes via a new unpublishHotplugVolume function that confirms volume removal from VMI.Status and forces detachment if needed. ControllerUnpublishVolume delegates non-NFS volumes to this path. Go.mod promotes kubevirt.io/api to a direct dependency and updates the csi-driver replace directive.

Changes

Hotplug Volume Unpublish Verification

Layer / File(s) Summary
Hotplug volume unpublish verification and cleanup
packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go
Introduces unpublishHotplugVolume function that post-processes upstream unpublish for hotplug volumes by verifying they are no longer in VMI.Status.VolumeStatus, forcing removal if still present via RemoveVolumeFromVMI, and waiting up to 2 minutes for EnsureVolumeRemoved. Updates ControllerUnpublishVolume to delegate non-NFS volumes to this new path instead of returning upstream response directly.
Kubevirt API dependency promotion
packages/apps/kubernetes/images/kubevirt-csi-driver/go.mod
Promotes kubevirt.io/api v1.2.2 from indirect to direct dependency in go.mod, and updates the kubevirt.io/csi-driver replace directive to use a newer pseudo-version that includes the unpublish verification changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • cozystack/cozystack#2658: Both PRs modify controller.go's ControllerUnpublishVolume non-NFS (RWX block) routing via an isNFSVolume-based branch, with the main PR further extending the non-NFS path using a dedicated unpublishHotplugVolume cleanup to remove stale hotplug volumes from the VMI—directly building on the upstream hotplug-detach routing fix from #2658.
  • cozystack/cozystack#2659: Both PRs modify controller.go to add hotplug (non-NFS) wrapper logic around the upstream CSI controller by re-reading VMI.Status.VolumeStatus—one verifies volume is ready after ControllerPublishVolume, the other ensures it's fully removed after ControllerUnpublishVolume.

Suggested labels

backport, area/kubernetes, area/storage, area/virtualization

Suggested reviewers

  • lllamnyp
  • androndo
  • IvanHunters
  • sircthulhu
  • lexfrei

Poem

🐰 A hotplug detached, yet lingered in sight,
So we verify volumes and force their flight,
VMI status checked, two minutes to wait,
Cleanup assured, the volume's sealed fate! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing detachment of orphan hot-plug volumes when a VMI outlives its VM, which is the core issue addressed in this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/kubevirt-csi-orphan-detach

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added size/M This PR changes 30-99 lines, ignoring generated files area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug labels Jun 10, 2026
@kvaps
Andrei Kvapil (kvaps) marked this pull request as ready for review June 10, 2026 13:49
@kvaps Andrei Kvapil (kvaps) removed the debug Debugging in progress label Jun 10, 2026
@kvaps Andrei Kvapil (kvaps) self-assigned this Jun 10, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue where hot-plug volumes remain exclusively attached to the source host during node reprovisioning when a VirtualMachineInstance outlives its parent VirtualMachine. By adding a verification step in the controller wrapper to check VMI volume status after the upstream unpublish process, the driver now correctly cleans up orphaned volumes, preventing subsequent attachment failures.

Highlights

  • Orphaned Volume Cleanup: Implemented a new unpublishHotplugVolume method to ensure hot-plug volumes are explicitly detached from the VMI if they remain attached after the upstream unpublish call, preventing storage device locking issues.
  • Dependency Update: Updated the kubevirt/csi-driver module to the latest version to incorporate upstream fixes.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new helper method unpublishHotplugVolume in the KubeVirt CSI driver controller to handle the detachment of hotplug volumes directly from the Virtual Machine Instance (VMI) if they remain attached after upstream unpublishing. It also updates the kubevirt.io/csi-driver dependency. A critical issue was identified in the namespace lookup within unpublishHotplugVolume: the code incorrectly queries the VMI in the infrastructure namespace (w.infraNamespace) instead of the tenant namespace parsed from the node ID, which would cause the lookup to fail and silently bypass the detachment logic.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +428 to +465
_, vmName, err := cache.SplitMetaNamespaceKey(req.GetNodeId())
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to parse node ID %q: %v", req.GetNodeId(), err)
}

// GetVirtualMachine on this client returns a VirtualMachineInstance, not a VM.
vmi, err := w.virtClient.GetVirtualMachine(ctx, w.infraNamespace, vmName)
if err != nil {
if errors.IsNotFound(err) {
return &csi.ControllerUnpublishVolumeResponse{}, nil
}
return nil, status.Errorf(codes.Unavailable,
"cannot verify VMI %s/%s after unpublish of %s: %v", w.infraNamespace, vmName, dvName, err)
}

stillAttached := false
for _, vs := range vmi.Status.VolumeStatus {
if vs.HotplugVolume != nil && vs.Name == dvName {
stillAttached = true
break
}
}
if !stillAttached {
return &csi.ControllerUnpublishVolumeResponse{}, nil
}

klog.Infof("Volume %s still reported by VMI %s/%s after unpublish – detaching from the VMI", dvName, w.infraNamespace, vmName)
if err := w.virtClient.RemoveVolumeFromVMI(ctx, w.infraNamespace, vmName, &kubevirtv1.RemoveVolumeOptions{Name: dvName}); err != nil {
return nil, status.Errorf(codes.Unavailable,
"failed to remove volume %s from VMI %s/%s: %v", dvName, w.infraNamespace, vmName, err)
}
if err := w.virtClient.EnsureVolumeRemoved(ctx, w.infraNamespace, vmName, dvName, 2*time.Minute); err != nil {
return nil, status.Errorf(codes.Unavailable,
"volume %s failed to be removed from VMI %s/%s in time: %v", dvName, w.infraNamespace, vmName, err)
}

klog.V(3).Infof("Successfully unpublished volume %s from VMI %s/%s", dvName, w.infraNamespace, vmName)
return &csi.ControllerUnpublishVolumeResponse{}, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

In unpublishHotplugVolume, the code parses the namespace and name of the VM/VMI from req.GetNodeId(), but discards the namespace (_) and instead uses w.infraNamespace to look up and modify the VMI.

In Cozystack, the VMI runs in the tenant namespace (which is vmNamespace parsed from the node ID), whereas w.infraNamespace is the namespace of the infrastructure cluster where the CSI driver and storage resources are located. Looking up the VMI in w.infraNamespace will result in a NotFound error, which is caught and silently ignored (returning success). This completely bypasses the orphan hot-plug volume detachment logic.

We should use vmNamespace instead of w.infraNamespace for all VMI operations in this function, matching the behavior in publishHotplugVolume.

	vmNamespace, vmName, err := cache.SplitMetaNamespaceKey(req.GetNodeId())
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to parse node ID %q: %v", req.GetNodeId(), err)
	}

	// GetVirtualMachine on this client returns a VirtualMachineInstance, not a VM.
	vmi, err := w.virtClient.GetVirtualMachine(ctx, vmNamespace, vmName)
	if err != nil {
		if errors.IsNotFound(err) {
			return &csi.ControllerUnpublishVolumeResponse{}, nil
		}
		return nil, status.Errorf(codes.Unavailable,
			"cannot verify VMI %s/%s after unpublish of %s: %v", vmNamespace, vmName, dvName, err)
	}

	stillAttached := false
	for _, vs := range vmi.Status.VolumeStatus {
		if vs.HotplugVolume != nil && vs.Name == dvName {
			stillAttached = true
			break
		}
	}
	if !stillAttached {
		return &csi.ControllerUnpublishVolumeResponse{}, nil
	}

	klog.Infof("Volume %s still reported by VMI %s/%s after unpublish – detaching from the VMI", dvName, vmNamespace, vmName)
	if err := w.virtClient.RemoveVolumeFromVMI(ctx, vmNamespace, vmName, &kubevirtv1.RemoveVolumeOptions{Name: dvName}); err != nil {
		return nil, status.Errorf(codes.Unavailable,
			"failed to remove volume %s from VMI %s/%s: %v", dvName, vmNamespace, vmName, err)
	}
	if err := w.virtClient.EnsureVolumeRemoved(ctx, vmNamespace, vmName, dvName, 2*time.Minute); err != nil {
		return nil, status.Errorf(codes.Unavailable,
			"volume %s failed to be removed from VMI %s/%s in time: %v", dvName, vmNamespace, vmName, err)
	}

	klog.V(3).Infof("Successfully unpublished volume %s from VMI %s/%s", dvName, vmNamespace, vmName)
	return &csi.ControllerUnpublishVolumeResponse{}, nil

@kvaps Andrei Kvapil (kvaps) added the backport Should change be backported on previous release label Jun 10, 2026
@kvaps
Andrei Kvapil (kvaps) merged commit 9eaadc8 into main Jun 22, 2026
23 of 24 checks passed
@kvaps
Andrei Kvapil (kvaps) deleted the fix/kubevirt-csi-orphan-detach branch June 22, 2026 09:32
@github-actions

Copy link
Copy Markdown

Successfully created backport PR for release-1.4:

Andrei Kvapil (kvaps) added a commit that referenced this pull request Jun 24, 2026
…VMI outlives its VM (#2965)

# Description
Backport of #2866 to `release-1.4`.
@lexfrei Aleksei Sviridkin (lexfrei) added area/storage Issues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor) and removed area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review labels Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Backport failed for release-1.5, because it was unable to cherry-pick the commit(s).

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin release-1.5
git worktree add -d .worktree/backport-2866-to-release-1.5 origin/release-1.5
cd .worktree/backport-2866-to-release-1.5
git switch --create backport-2866-to-release-1.5
git cherry-pick -x 6b4d7adb2c9ba96a80a34b44680d2769a7cc10f7 68136f9292f8f6a877ea0ce137ed97caf81f2a4a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/storage Issues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor) backport Should change be backported on previous release kind/bug Categorizes issue or PR as related to a bug size/M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants