11// © Broadcom. All Rights Reserved.
2- // The term “ Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
2+ // The term " Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
33// SPDX-License-Identifier: Apache-2.0
44
55package virtualmachine
@@ -10,13 +10,17 @@ import (
1010 "path"
1111
1212 "github.com/go-logr/logr"
13+ "github.com/vmware/govmomi/cns"
14+ cnstypes "github.com/vmware/govmomi/cns/types"
1315 "github.com/vmware/govmomi/fault"
1416 "github.com/vmware/govmomi/object"
1517 "github.com/vmware/govmomi/vim25"
1618 vimtypes "github.com/vmware/govmomi/vim25/types"
19+ "github.com/vmware/govmomi/vslm"
1720
1821 pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants"
1922 pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context"
23+ pkglog "github.com/vmware-tanzu/vm-operator/pkg/log"
2024 vmutil "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/vm"
2125)
2226
@@ -26,6 +30,7 @@ var VMDeletePropertiesSelector = []string{
2630 "recentTask" ,
2731 "config.extraConfig" ,
2832 "config.files" ,
33+ "config.hardware.device" ,
2934 "summary.runtime.connectionState" ,
3035}
3136
@@ -94,6 +99,18 @@ func DeleteVirtualMachine(
9499 return fmt .Errorf ("destroy VM task failed: %w" , err )
95100 }
96101
102+ // After Destroy(), delete any FCD-backed disks left in the VM's directory
103+ // (left behind because keepAfterDeleteVm=true) via the CNS DeleteVolume
104+ // API. This MUST run after Destroy() so the disk is no longer attached
105+ // and CNS permits the delete. Using the CNS API (not raw VSLM) keeps
106+ // CNS's internal state consistent so the subsequent PVC cleanup path
107+ // handles any duplicate delete gracefully with no VC UI error.
108+ // See deleteFCDsInVMDir for full context.
109+ deleteFCDsInVMDir (
110+ vmCtx ,
111+ vcVM .Client (),
112+ vmCtx .MoVM .Config )
113+
97114 // Best-effort cleanup of VM dir after Destroy();
98115 // no error return since delete is still considered successful.
99116 if dc == nil ||
@@ -112,3 +129,160 @@ func DeleteVirtualMachine(
112129 namespacePath )
113130 return nil
114131}
132+
133+ // deleteFCDsInVMDir finds all FCD-backed disks in the VM's own directory and
134+ // deletes them via the CNS DeleteVolume API (with deleteDisk=true) before the
135+ // VM is destroyed.
136+ //
137+ // The unmanaged volumes registration process (unmanagedvolumes_register)
138+ // registers a VM's ephemeral disks as CNS FCDs via CnsRegisterVolume, which
139+ // sets keepAfterDeleteVm=true. During VM deletion, vcVM.Destroy() skips such
140+ // disks because of this flag. CleanupVMDir() then removes the directory via
141+ // the raw FileManager API without updating the FCD catalog, leaving a stale
142+ // catalog entry pointing to a now-deleted file.
143+ //
144+ // The CNS DeleteVolume API (rather than the lower-level VSLM
145+ // DeleteVStorageObject) is used because CNS tracks the deletion in its own
146+ // internal state. When CNS's PVC cleanup path subsequently also tries to
147+ // delete the same volume, CNS recognises it is already gone and handles the
148+ // duplicate call gracefully — avoiding "not found" errors visible in the
149+ // vCenter task UI.
150+ //
151+ // Only disks whose backing file is in the VM's own directory are affected;
152+ // user-attached PVC disks in separate directories are not touched.
153+ //
154+ // Note: after CSI registers a VMDK as an FCD, the VM hardware config's
155+ // backingObjectId field is not updated. The VSLM catalog is queried directly
156+ // to find FCDs whose backing file path matches the VM's disk paths.
157+ func deleteFCDsInVMDir (
158+ ctx context.Context ,
159+ vimClient * vim25.Client ,
160+ config * vimtypes.VirtualMachineConfigInfo ) {
161+
162+ if config == nil {
163+ return
164+ }
165+ if vimClient .ServiceContent .VStorageObjectManager == nil {
166+ return
167+ }
168+
169+ vmDir := path .Dir (config .Files .VmPathName )
170+ logger := pkglog .FromContextOrDefault (ctx )
171+
172+ logger .Info (
173+ "Scanning VM disks for FCD cleanup" ,
174+ "vmDir" , vmDir ,
175+ "numDevices" , len (config .Hardware .Device ))
176+
177+ // Collect the file paths of all disks in the VM's own directory, and
178+ // capture one datastore MoRef to use for VSLM queries.
179+ diskFilesInVMDir := map [string ]struct {}{}
180+ var datastoreMoRef * vimtypes.ManagedObjectReference
181+ for _ , dev := range config .Hardware .Device {
182+ disk , ok := dev .(* vimtypes.VirtualDisk )
183+ if ! ok {
184+ continue
185+ }
186+ bi := getVirtualDiskFileBackingInfo (disk )
187+ if bi == nil || bi .FileName == "" || bi .Datastore == nil {
188+ continue
189+ }
190+ logger .Info (
191+ "Found disk in VM" ,
192+ "fileName" , bi .FileName ,
193+ "inVMDir" , path .Dir (bi .FileName ) == vmDir )
194+ if path .Dir (bi .FileName ) != vmDir {
195+ continue
196+ }
197+ diskFilesInVMDir [bi .FileName ] = struct {}{}
198+ datastoreMoRef = bi .Datastore
199+ }
200+
201+ if len (diskFilesInVMDir ) == 0 || datastoreMoRef == nil {
202+ logger .Info (
203+ "No disks found in VM directory, skipping FCD cleanup" ,
204+ "vmDir" , vmDir )
205+ return
206+ }
207+
208+ // List all FCDs on the datastore and find those whose backing file
209+ // matches a disk in the VM's directory.
210+ mgr := vslm .NewObjectManager (vimClient )
211+ ds := object .NewDatastore (vimClient , * datastoreMoRef )
212+
213+ ids , err := mgr .List (ctx , ds )
214+ if err != nil {
215+ logger .Error (err ,
216+ "Failed to list FCDs on datastore for cleanup" )
217+ return
218+ }
219+
220+ // Build CNS client once; used to delete matched FCDs via CNS DeleteVolume
221+ // so that CNS internal state is updated and its PVC cleanup path handles
222+ // the subsequent duplicate delete gracefully (no VC UI errors).
223+ cnsClient , err := cns .NewClient (ctx , vimClient )
224+ if err != nil {
225+ logger .Error (err , "Failed to create CNS client for FCD cleanup" )
226+ return
227+ }
228+
229+ for _ , id := range ids {
230+ obj , err := mgr .Retrieve (ctx , ds , id .Id )
231+ if err != nil {
232+ logger .Info (
233+ "Failed to retrieve FCD, skipping" ,
234+ "fcdID" , id .Id ,
235+ "err" , err )
236+ continue
237+ }
238+
239+ // Extract the backing file path.
240+ fb , ok := obj .Config .Backing .(
241+ vimtypes.BaseBaseConfigInfoFileBackingInfo )
242+ if ! ok {
243+ continue
244+ }
245+ filePath := fb .GetBaseConfigInfoFileBackingInfo ().FilePath
246+
247+ if _ , found := diskFilesInVMDir [filePath ]; ! found {
248+ continue
249+ }
250+
251+ logger .Info (
252+ "Deleting FCD disk in VM directory via CNS" ,
253+ "fcdID" , id .Id ,
254+ "diskFile" , filePath )
255+
256+ task , err := cnsClient .DeleteVolume (
257+ ctx ,
258+ []cnstypes.CnsVolumeId {{Id : id .Id }},
259+ true )
260+ if err != nil {
261+ logger .Error (err ,
262+ "Failed to initiate CNS volume delete" ,
263+ "fcdID" , id .Id ,
264+ "diskFile" , filePath )
265+ continue
266+ }
267+ if _ , err := task .WaitForResult (ctx ); err != nil {
268+ logger .Error (err ,
269+ "CNS volume delete task failed" ,
270+ "fcdID" , id .Id ,
271+ "diskFile" , filePath )
272+ }
273+ }
274+ }
275+
276+ // getVirtualDiskFileBackingInfo returns the VirtualDeviceFileBackingInfo for
277+ // a virtual disk if the disk uses file-based backing, otherwise nil.
278+ func getVirtualDiskFileBackingInfo (
279+ disk * vimtypes.VirtualDisk ) * vimtypes.VirtualDeviceFileBackingInfo {
280+
281+ type hasFileBackingInfo interface {
282+ GetVirtualDeviceFileBackingInfo () * vimtypes.VirtualDeviceFileBackingInfo
283+ }
284+ if fb , ok := disk .Backing .(hasFileBackingInfo ); ok {
285+ return fb .GetVirtualDeviceFileBackingInfo ()
286+ }
287+ return nil
288+ }
0 commit comments