Skip to content

Check VMI status when detaching, not just VM spec - #184

Merged
kubevirt-prow[bot] merged 1 commit into
kubevirt:mainfrom
mattia-eleuteri:fix/orphan-hotplug-vmi-check
Jun 24, 2026
Merged

kubevirt-prow[bot] merged 1 commit into
kubevirt:mainfrom
mattia-eleuteri:fix/orphan-hotplug-vmi-check

Conversation

@mattia-eleuteri

@mattia-eleuteri mattia-eleuteri commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

ControllerUnpublishVolume currently returns success as soon as the parent VirtualMachine object is missing, on the assumption that a missing VM means the hot-plug is already gone. In practice the VMI (and its hot-plug pod) can outlive the parent VM during a CAPI/CAPK teardown race — the infra-side storage device then stays exclusively attached to the source host, and the next attempt to attach the same volume to a different VM fails with Multi-Attach error or, on backends like DRBD that enforce exclusive attachment, with failed to set source device readwrite.

The same divergence appears when virt-api updates VM.spec optimistically on a removevolume call but virt-handler has not yet unplugged the QEMU device.

This PR:

  • Adds EnsureVolumeRemovedVMI to the kubevirt.Client interface, mirroring the existing EnsureVolumeRemovedVM but inspecting VMI.status.volumeStatus instead of VM.spec.template.spec.volumes.
  • Replaces the VM not found = success short-circuit in ControllerUnpublishVolume with a new volumeStillAttached helper that consults both surfaces. The original During detach don't return error if VM is not found #119 unblock is preserved (the call still succeeds when the volume is genuinely gone from both); the new behaviour is that we now actually detach from the VMI when it still holds the hot-plug.
  • Refactors removeVolumeFromVm so that it falls through to RemoveVolumeFromVMI when the parent VM is gone but the VMI still reports the hot-plug, closing the orphan path end-to-end.
  • Adds a regression test in controller_test.go that covers the "VM gone but VMI still has the hot-plug" path.

We hit the bug 12 times across two tenant clusters in a single day before opening the linked issue. Full failure-mode catalogue and reproducer are in the issue.

Which issue(s) this PR fixes:

Fixes #182
Refs #83 #119 #134

Special notes for your reviewer:

  • This changes behaviour: a "VM not found" no longer means "detach succeeded". For existing deployments that have already lost the VM object and only have the orphan VMI left, ControllerUnpublishVolume will now actually run RemoveVolumeFromVMI against the VMI — i.e. it will do the right thing instead of silently doing nothing. There is no CSI/API-surface change.
  • I added one method (EnsureVolumeRemovedVMI) to the kubevirt.Client interface. I did not update pkg/kubevirt/mocked_client.go: the file is already stale relative to the interface (missing EnsureVolumeRemovedVM and EnsureVolumeAvailableVM among others) and make mockgen produces a much larger diff than the orphan-volume fix justifies. A separate "regen mocks" PR would be the right home for that. The hand-written test fakes (ControllerClientMock in pkg/service/controller_test.go and fakeKubeVirtClient in sanity/sanity_test.go) are updated.
  • A companion PR (Add opt-in DetachReconciler for orphan hot-plug volumes #185, separate and independent) introduces an opt-in DetachReconciler to catch orphans this synchronous path cannot reach (external-attacher giving up after force-detach timeout, VMIs in mid-migration, legacy state accumulated before this fix). They can be reviewed and merged in any order.

Release note:

BugFix: ControllerUnpublishVolume no longer treats a missing parent VirtualMachine as "detach succeeded" — it now also checks the VMI status and detaches the hot-plug from the VMI if one is still attached, preventing orphan hot-plug pods that pin the infra storage device exclusively to the source host.

@kubevirt-bot kubevirt-bot added the release-note Denotes a PR that will be considered when it comes time to generate release notes. label Jun 4, 2026
@kubevirt-bot
kubevirt-bot requested review from awels and davidvossel June 4, 2026 14:28
@kubevirt-bot

Copy link
Copy Markdown

Hi @mattia-eleuteri. Thanks for your PR.

PRs from untrusted users cannot be marked as trusted with /ok-to-test in this repo meaning untrusted PR authors can never trigger tests themselves. Collaborators can still trigger tests on the PR using /test all.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@kubevirt-bot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign mhenriks for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@mattia-eleuteri

Copy link
Copy Markdown
Contributor Author

Reopening to retrigger DCO check (the check didn't fire on initial open, likely a webhook race).

ControllerUnpublishVolume used to return success as soon as the parent
VirtualMachine was missing, on the assumption that a missing VM meant
the hot-plug was already gone. In practice the VMI (and its hot-plug
pod) can outlive the parent VM during a CAPI/CAPK teardown race, so the
infra-side storage device stays exclusively attached to the source host.
The next attempt to attach the same volume to a different VM then fails
with 'Multi-Attach error' from the attach/detach controller or, on
storage backends that enforce exclusive attachment at the device layer
(e.g. DRBD), with 'failed to set source device readwrite'.

The same divergence happens when virt-api updates VM.spec optimistically
on a removevolume call but virt-handler has not finished unplugging the
QEMU device yet — VM.spec no longer references the volume, but the VMI
status still does.

This commit makes ControllerUnpublishVolume consult both surfaces:

  - EnsureVolumeRemovedVMI (new) reports whether the volume is still in
    VMI.Status.VolumeStatus as a HotplugVolume.
  - The new volumeStillAttached helper combines the existing VM-spec
    check with the new VMI-status check and only short-circuits to
    success when both agree the volume is gone.

removeVolumeFromVm is refactored to fall through to RemoveVolumeFromVMI
when the parent VM is gone but the VMI still reports the hot-plug,
closing the orphan path end-to-end.

Refs: kubevirt#83, kubevirt#119, kubevirt#134.
Signed-off-by: Mattia Eleuteri <[email protected]>
@mattia-eleuteri
mattia-eleuteri force-pushed the fix/orphan-hotplug-vmi-check branch from a2332e3 to dda9996 Compare June 4, 2026 14:35
@kubevirt-bot kubevirt-bot added the dco-signoff: yes Indicates the PR's author has DCO signed all their commits. label Jun 4, 2026

@kvaps kvaps left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The diagnosis is correct: ControllerUnpublishVolume currently short-circuits to success as soon as the parent VM is gone (and even returns nil, nil there), so an orphan VMI keeps its hot-plug pod alive and the infra device stays attached to the source host. Checking both surfaces (VM.spec via EnsureVolumeRemovedVM and VMI.status.volumeStatus via the new EnsureVolumeRemovedVMI) and falling through to RemoveVolumeFromVMI when the VM is missing is the right fix, and the final EnsureVolumeRemoved wait was already VMI-aware, so the detach semantics stay consistent end-to-end.

One behavioural note, not a blocker: during a normal unplug, after the volume is gone from VM.spec but before virt-handler finishes the unplug, a retried unpublish now goes down the VMI branch and calls the removevolume subresource against a volume that is already mid-unplug. The subresource will likely reject that, the backoff loop swallows the error and the external-attacher retries, so it converges — just expect some extra log noise compared to the old (incorrectly optimistic) fast path.

Not regenerating mocked_client.go seems reasonable given the file is already stale relative to the interface; a separate mock-regen PR is the right place for that.

LGTM overall — we hit this exact failure mode with DRBD-backed storage, where the leaked hot-plug pod keeps the device exclusively attached and every subsequent attach fails with failed to set source device readwrite.

@kubevirt-bot

Copy link
Copy Markdown

@mattia-eleuteri: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-csi-driver-split-e2e-k8s dda9996 link true /test pull-csi-driver-split-e2e-k8s
Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@kubevirt-bot

Copy link
Copy Markdown

Pull requests that are marked with lgtm should receive a review
from an approver within 1 week.

After that period the bot marks them with the label needs-approver-review.

/label needs-approver-review

@kubevirt-bot kubevirt-bot added the needs-approver-review Indicates that a PR requires a review from an approver. label Jun 17, 2026
kvaps added a commit to cozystack/cozystack that referenced this pull request Jun 22, 2026
)

## 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 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

```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
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
pull Bot pushed a commit to medampudi/cozystack that referenced this pull request Jun 22, 2026
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 <[email protected]>
Signed-off-by: Andrei Kvapil <[email protected]>
@kvaps

kvaps commented Jun 24, 2026

Copy link
Copy Markdown
Member

/retest

@awels awels left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

/approve

@kubevirt-prow

kubevirt-prow Bot commented Jun 24, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: awels

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubevirt-prow kubevirt-prow Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jun 24, 2026
@kubevirt-prow
kubevirt-prow Bot merged commit 27b52aa into kubevirt:main Jun 24, 2026
8 checks passed
@kubevirt-bot

Copy link
Copy Markdown

/remove-label needs-approver-review

@kubevirt-prow kubevirt-prow Bot removed the needs-approver-review Indicates that a PR requires a review from an approver. label Jun 24, 2026
akalenyu pushed a commit to akalenyu/csi-driver that referenced this pull request Aug 3, 2026
ControllerUnpublishVolume used to return success as soon as the parent
VirtualMachine was missing, on the assumption that a missing VM meant
the hot-plug was already gone. In practice the VMI (and its hot-plug
pod) can outlive the parent VM during a CAPI/CAPK teardown race, so the
infra-side storage device stays exclusively attached to the source host.
The next attempt to attach the same volume to a different VM then fails
with 'Multi-Attach error' from the attach/detach controller or, on
storage backends that enforce exclusive attachment at the device layer
(e.g. DRBD), with 'failed to set source device readwrite'.

The same divergence happens when virt-api updates VM.spec optimistically
on a removevolume call but virt-handler has not finished unplugging the
QEMU device yet — VM.spec no longer references the volume, but the VMI
status still does.

This commit makes ControllerUnpublishVolume consult both surfaces:

  - EnsureVolumeRemovedVMI (new) reports whether the volume is still in
    VMI.Status.VolumeStatus as a HotplugVolume.
  - The new volumeStillAttached helper combines the existing VM-spec
    check with the new VMI-status check and only short-circuits to
    success when both agree the volume is gone.

removeVolumeFromVm is refactored to fall through to RemoveVolumeFromVMI
when the parent VM is gone but the VMI still reports the hot-plug,
closing the orphan path end-to-end.

Refs: kubevirt#83, kubevirt#119, kubevirt#134.

Signed-off-by: Mattia Eleuteri <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 24, 2026
kubevirt/csi-driver cb4770e4e475 adds the flag so the driver can run
against KubeVirt without the deprecated HotplugVolumes gate: with it
off, a volume missing from VM.spec is not unplugged through the
VMI-level removevolume on a VM-owned VMI, which virt-api rejects once
that gate is gone. Bump to it and hand the flag to the upstream
controller service. The flag defaults to true, which keeps the detach
path as it was.

The wrapper's own hot-unplug override goes. It was a backport of
kubevirt/csi-driver#184, and the previous pin already contained that
change: upstream detaches from the VMI when the VM is gone and waits
for the VMI status to drop the volume before returning, so the
wrapper's re-check never found it attached. Keeping it would have
meant a second copy of the new ownership rule behind a branch nothing
reaches.

The bump also brings in the upstream node-service changes merged since
the previous pin: NodeUnpublishVolume now uses CleanupMountPoint
instead of Unmount (kubevirt/csi-driver#378), and device lookup runs
udevadm trigger on block devices without a serial
(kubevirt/csi-driver#363). The image carries no udevadm, so that rescan
only logs an error and the lookup behaves as before.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 24, 2026
kubevirt/csi-driver cb4770e4e475 adds the flag so the driver can run
against KubeVirt without the deprecated HotplugVolumes gate: with it
off, a volume missing from VM.spec is not unplugged through the
VMI-level removevolume on a VM-owned VMI, which virt-api rejects once
that gate is gone. Bump to it and hand the flag to the upstream
controller service. The flag defaults to true, which keeps the detach
path as it was.

The wrapper's own hot-unplug override goes. It was a backport of
kubevirt/csi-driver#184, and the previous pin already contained that
change: upstream detaches from the VMI when the VM is gone and waits
for the VMI status to drop the volume before returning, so the
wrapper's re-check never found it attached. Keeping it would have
meant a second copy of the new ownership rule behind a branch nothing
reaches.

The bump also brings in the upstream node-service changes merged since
the previous pin: NodeUnpublishVolume now uses CleanupMountPoint
instead of Unmount (kubevirt/csi-driver#378), and device lookup runs
udevadm trigger on block devices without a serial
(kubevirt/csi-driver#363). The image carries no udevadm, so that rescan
only logs an error and the lookup behaves as before.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 28, 2026
kubevirt/csi-driver cb4770e4e475 adds the flag so the driver can run
against KubeVirt without the deprecated HotplugVolumes gate: with it
off, a volume missing from VM.spec is not unplugged through the
VMI-level removevolume on a VM-owned VMI, which virt-api rejects once
that gate is gone. Bump to it and hand the flag to the upstream
controller service. The flag defaults to true, which keeps the detach
path as it was. cb4770e4e475 is the previous pin plus that one change.

The wrapper's own hot-unplug override goes. It was a backport of
kubevirt/csi-driver#184, and the previous pin already contained that
change: upstream detaches from the VMI when the VM is gone and waits
for the VMI status to drop the volume before returning, so the
wrapper's re-check never found it attached. Keeping it would have
meant a second copy of the new ownership rule behind a branch nothing
reaches.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
lexfrei added a commit to cozystack/cozystack that referenced this pull request Sep 28, 2026
kubevirt/csi-driver cb4770e4e475 adds the flag so the driver can run
against KubeVirt without the deprecated HotplugVolumes gate: with it
off, a volume missing from VM.spec is not unplugged through the
VMI-level removevolume on a VM-owned VMI, which virt-api rejects once
that gate is gone. Bump to it and hand the flag to the upstream
controller service. The flag defaults to true, which keeps the detach
path as it was. cb4770e4e475 is the previous pin plus that one change.

The wrapper's own hot-unplug override goes. It was a backport of
kubevirt/csi-driver#184, and the previous pin already contained that
change: upstream detaches from the VMI when the VM is gone and waits
for the VMI status to drop the volume before returning, so the
wrapper's re-check never found it attached. Keeping it would have
meant a second copy of the new ownership rule behind a branch nothing
reaches.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
IvanHunters added a commit to cozystack/cozystack that referenced this pull request Oct 5, 2026
…gate (#4451)

## What this PR does

This lets kubevirt-csi detach volumes without the deprecated KubeVirt
`HotplugVolumes` gate. The default stays as it is, so nothing changes at
runtime yet. Part of #4032.

The driver got a switch upstream for exactly this, and I bumped to it.
kubevirt/csi-driver#382 added `--enable-vmi-hotplug-fallback`, merged as
cb4770e4e475. With it set to false, the driver no longer calls the
VMI-level `removevolume` on a VMI owned by a VM. virt-api rejects that
call once `HotplugVolumes` is off.

Our wrapper now passes the flag to upstream and no longer has its own
hot-unplug path. `unpublishHotplugVolume` was a backport of
kubevirt/csi-driver#184, and the previous pin already contained that
change. Upstream detaches from the VMI when the VM is gone, then waits
for the VMI status to drop the volume before it returns. So the
wrapper's re-check never found the volume attached. Adding the ownership
check there would have been dead code.

Closes #4450

The chart leaves the flag at its default of true and does not render it.
The driver image in the chart is stamped only at release preparation, so
between releases main runs the previous release's binary, which does not
know the flag. Go's `flag` package exits on an unknown flag. Rendering
`=true` would crash-loop the CSI controller for no effect. The right
value depends on the KubeVirt CR, not on the tenant, so the comment next
to `HotplugVolumes` in `kubevirt-cr.yaml` points at the controller
Deployment. The follow-up PR drops the gate and renders the flag as
false.

Nothing changes on the node side. Main already pins f5de10d39556, and
cb4770e4e475 is that commit plus kubevirt/csi-driver#382.

A Go unit test runs an unpublish through the wrapper during a teardown,
with the flag on, off with a VM-owned VMI, and off with an orphaned VMI.
It checks that the flag reaches upstream and that only upstream makes
the VMI-level call. Upstream's own tests cover the ownership rule. A
second test holds that an NFS volume only releases the node from its
CiliumNetworkPolicy and never reaches the upstream hot-unplug. A
helm-unittest pins that the chart renders no such argument. I haven't
built the image or run e2e. Volume attach and detach on a tenant cluster
in e2e is the real check.

### Screenshots

No UI change.

### Downstream repositories

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

### Release note

```release-note
feat(kubernetes): kubevirt-csi supports --enable-vmi-hotplug-fallback, so it can detach volumes without the KubeVirt HotplugVolumes feature gate. The flag stays true for now and behaviour is unchanged.
```
kubevirt-prow Bot pushed a commit that referenced this pull request Oct 22, 2026
)

removeVolumeFromVm treats NotFound from GetWorkloadManagingVirtualMachine
as "no VM", but then checks vm != nil. A typed client-go Get returns an
empty, non-nil object together with NotFound, so the check passes and
vm.Spec.Template, which is nil, is dereferenced. ControllerUnpublishVolume
panics on every retry for a VMI that outlives its VM, and the volume stays
on the VMI: the case the VMI fallback path was added for (#182, #184).

Reset vm to nil on NotFound. The test mock now returns the empty object a
real client returns, which makes the existing VM-gone tests panic without
the fix.

Signed-off-by: Moep90 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. dco-signoff: yes Indicates the PR's author has DCO signed all their commits. lgtm Indicates that a PR is ready to be merged. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ControllerUnpublishVolume leaks orphan hot-plug volumes when VM teardown precedes VMI termination

4 participants