@@ -40,6 +40,7 @@ import (
4040 vmmModels "github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4/models/vmm/v4/ahv/config"
4141 policyModels "github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4/models/vmm/v4/ahv/policies"
4242 imageModels "github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4/models/vmm/v4/content"
43+ volumesconfig "github.com/nutanix/ntnx-api-golang-clients/volumes-go-client/v4/models/volumes/v4/config"
4344
4445 . "github.com/onsi/ginkgo/v2"
4546 . "github.com/onsi/gomega"
@@ -2299,6 +2300,215 @@ func TestGpuVendorStringToGpuVendor(t *testing.T) {
22992300 }
23002301}
23012302
2303+ func TestDetachVolumeGroupsFromVM (t * testing.T ) {
2304+ ctrl := gomock .NewController (t )
2305+ defer ctrl .Finish ()
2306+
2307+ ctx := context .Background ()
2308+
2309+ t .Run ("should skip detachment when no disks provided" , func (t * testing.T ) {
2310+ mockClientWrapper := NewMockConvergedClient (ctrl )
2311+
2312+ vmName := "test-vm"
2313+ vmUUID := "00000000-0000-0000-0000-000000000001"
2314+ vmDisks := []vmmModels.Disk {}
2315+
2316+ // No expectations: DetachFromVM should NOT be called
2317+ err := detachVolumeGroupsFromVM (ctx , mockClientWrapper .Client , vmName , vmUUID , vmDisks )
2318+
2319+ assert .NoError (t , err )
2320+ })
2321+
2322+ t .Run ("should skip detachment when disk has no backing info" , func (t * testing.T ) {
2323+ mockClientWrapper := NewMockConvergedClient (ctrl )
2324+
2325+ vmName := "test-vm"
2326+ vmUUID := "00000000-0000-0000-0000-000000000001"
2327+
2328+ // Disk without VG backing (leave BackingInfo nil)
2329+ disk := vmmModels .NewDisk ()
2330+ vmDisks := []vmmModels.Disk {* disk }
2331+
2332+ // No expectations: DetachFromVM should NOT be called
2333+ err := detachVolumeGroupsFromVM (ctx , mockClientWrapper .Client , vmName , vmUUID , vmDisks )
2334+
2335+ assert .NoError (t , err )
2336+ })
2337+
2338+ t .Run ("should skip detachment when disk is not backed by volume group" , func (t * testing.T ) {
2339+ mockClientWrapper := NewMockConvergedClient (ctrl )
2340+
2341+ vmName := "test-vm"
2342+ vmUUID := "00000000-0000-0000-0000-000000000001"
2343+
2344+ // Disk backed by VmDisk (not volume group)
2345+ disk := vmmModels .NewDisk ()
2346+ vmDisk := vmmModels .NewVmDisk ()
2347+ vmDisk .DiskSizeBytes = ptr .To (int64 (21474836480 ))
2348+ err := disk .SetBackingInfo (* vmDisk )
2349+ require .NoError (t , err )
2350+
2351+ vmDisks := []vmmModels.Disk {* disk }
2352+
2353+ // No expectations: DetachFromVM should NOT be called
2354+ err = detachVolumeGroupsFromVM (ctx , mockClientWrapper .Client , vmName , vmUUID , vmDisks )
2355+
2356+ assert .NoError (t , err )
2357+ })
2358+
2359+ t .Run ("should successfully detach single volume group" , func (t * testing.T ) {
2360+ mockClientWrapper := NewMockConvergedClient (ctrl )
2361+
2362+ vmName := "test-vm"
2363+ vmUUID := "00000000-0000-0000-0000-000000000001"
2364+ vgID := "11111111-1111-1111-1111-111111111111"
2365+
2366+ // Build a Disk backed by an ADSFVolumeGroupReference
2367+ disk := vmmModels .NewDisk ()
2368+ ref := vmmModels .NewADSFVolumeGroupReference ()
2369+ ref .VolumeGroupExtId = & vgID
2370+ err := disk .SetBackingInfo (* ref )
2371+ require .NoError (t , err )
2372+
2373+ vmDisks := []vmmModels.Disk {* disk }
2374+
2375+ expectedVG := & volumesconfig.VolumeGroup {
2376+ ExtId : ptr .To (vgID ),
2377+ }
2378+
2379+ // Expect DetachFromVM to be called once with the correct VG ID
2380+ mockClientWrapper .MockVolumeGroups .EXPECT ().
2381+ DetachFromVM (ctx , vgID , gomock .Any ()).
2382+ DoAndReturn (func (_ context.Context , _ string , attachment volumesconfig.VmAttachment ) (* volumesconfig.VolumeGroup , error ) {
2383+ assert .Equal (t , vmUUID , * attachment .ExtId )
2384+ return expectedVG , nil
2385+ })
2386+
2387+ err = detachVolumeGroupsFromVM (ctx , mockClientWrapper .Client , vmName , vmUUID , vmDisks )
2388+
2389+ assert .NoError (t , err )
2390+ })
2391+
2392+ t .Run ("should return error when DetachFromVM fails" , func (t * testing.T ) {
2393+ mockClientWrapper := NewMockConvergedClient (ctrl )
2394+
2395+ vmName := "test-vm"
2396+ vmUUID := "00000000-0000-0000-0000-000000000001"
2397+ vgID := "22222222-2222-2222-2222-222222222222"
2398+
2399+ disk := vmmModels .NewDisk ()
2400+ ref := vmmModels .NewADSFVolumeGroupReference ()
2401+ ref .VolumeGroupExtId = & vgID
2402+ err := disk .SetBackingInfo (* ref )
2403+ require .NoError (t , err )
2404+
2405+ vmDisks := []vmmModels.Disk {* disk }
2406+
2407+ expectedError := errors .New ("failed to detach volume group" )
2408+
2409+ // Return error from DetachFromVM
2410+ mockClientWrapper .MockVolumeGroups .EXPECT ().
2411+ DetachFromVM (ctx , vgID , gomock .Any ()).
2412+ Return (nil , expectedError )
2413+
2414+ err = detachVolumeGroupsFromVM (ctx , mockClientWrapper .Client , vmName , vmUUID , vmDisks )
2415+
2416+ assert .Error (t , err )
2417+ assert .Contains (t , err .Error (), "failed to detach volume group" )
2418+ assert .Contains (t , err .Error (), vgID )
2419+ assert .Contains (t , err .Error (), vmUUID )
2420+ })
2421+
2422+ t .Run ("should return after detaching first volume group" , func (t * testing.T ) {
2423+ mockClientWrapper := NewMockConvergedClient (ctrl )
2424+
2425+ vmName := "test-vm"
2426+ vmUUID := "00000000-0000-0000-0000-000000000001"
2427+ vgID1 := "11111111-1111-1111-1111-111111111111"
2428+ vgID2 := "22222222-2222-2222-2222-222222222222"
2429+
2430+ // Build two disks backed by volume groups
2431+ disk1 := vmmModels .NewDisk ()
2432+ ref1 := vmmModels .NewADSFVolumeGroupReference ()
2433+ ref1 .VolumeGroupExtId = & vgID1
2434+ err := disk1 .SetBackingInfo (* ref1 )
2435+ require .NoError (t , err )
2436+
2437+ disk2 := vmmModels .NewDisk ()
2438+ ref2 := vmmModels .NewADSFVolumeGroupReference ()
2439+ ref2 .VolumeGroupExtId = & vgID2
2440+ err = disk2 .SetBackingInfo (* ref2 )
2441+ require .NoError (t , err )
2442+
2443+ vmDisks := []vmmModels.Disk {* disk1 , * disk2 }
2444+
2445+ // Only expect DetachFromVM to be called once (for the first VG)
2446+ // The function returns after the first detachment
2447+ mockClientWrapper .MockVolumeGroups .EXPECT ().
2448+ DetachFromVM (ctx , vgID1 , gomock .Any ()).
2449+ Return (& volumesconfig.VolumeGroup {}, nil ).
2450+ Times (1 )
2451+
2452+ err = detachVolumeGroupsFromVM (ctx , mockClientWrapper .Client , vmName , vmUUID , vmDisks )
2453+
2454+ assert .NoError (t , err )
2455+ })
2456+
2457+ t .Run ("should handle mixed disk types and only detach volume groups" , func (t * testing.T ) {
2458+ mockClientWrapper := NewMockConvergedClient (ctrl )
2459+
2460+ vmName := "test-vm"
2461+ vmUUID := "00000000-0000-0000-0000-000000000001"
2462+ vgID := "11111111-1111-1111-1111-111111111111"
2463+
2464+ // First disk: regular VmDisk (should be skipped)
2465+ disk1 := vmmModels .NewDisk ()
2466+ vmDisk := vmmModels .NewVmDisk ()
2467+ vmDisk .DiskSizeBytes = ptr .To (int64 (21474836480 ))
2468+ err := disk1 .SetBackingInfo (* vmDisk )
2469+ require .NoError (t , err )
2470+
2471+ // Second disk: backed by volume group (should be detached)
2472+ disk2 := vmmModels .NewDisk ()
2473+ ref := vmmModels .NewADSFVolumeGroupReference ()
2474+ ref .VolumeGroupExtId = & vgID
2475+ err = disk2 .SetBackingInfo (* ref )
2476+ require .NoError (t , err )
2477+
2478+ vmDisks := []vmmModels.Disk {* disk1 , * disk2 }
2479+
2480+ // Only expect DetachFromVM to be called for the volume group disk
2481+ mockClientWrapper .MockVolumeGroups .EXPECT ().
2482+ DetachFromVM (ctx , vgID , gomock .Any ()).
2483+ Return (& volumesconfig.VolumeGroup {}, nil )
2484+
2485+ err = detachVolumeGroupsFromVM (ctx , mockClientWrapper .Client , vmName , vmUUID , vmDisks )
2486+
2487+ assert .NoError (t , err )
2488+ })
2489+
2490+ t .Run ("should handle nil volume group ExtId gracefully" , func (t * testing.T ) {
2491+ mockClientWrapper := NewMockConvergedClient (ctrl )
2492+
2493+ vmName := "test-vm"
2494+ vmUUID := "00000000-0000-0000-0000-000000000001"
2495+
2496+ // Build a disk with volume group reference but nil ExtId
2497+ disk := vmmModels .NewDisk ()
2498+ ref := vmmModels .NewADSFVolumeGroupReference ()
2499+ ref .VolumeGroupExtId = nil // Nil ExtId should cause panic or error
2500+ err := disk .SetBackingInfo (* ref )
2501+ require .NoError (t , err )
2502+
2503+ vmDisks := []vmmModels.Disk {* disk }
2504+
2505+ // This should panic when trying to dereference nil VolumeGroupExtId
2506+ assert .Panics (t , func () {
2507+ _ = detachVolumeGroupsFromVM (ctx , mockClientWrapper .Client , vmName , vmUUID , vmDisks )
2508+ })
2509+ })
2510+ }
2511+
23022512type MockConvergedClientWrapper struct {
23032513 Client * v4Converged.Client
23042514
@@ -2310,6 +2520,7 @@ type MockConvergedClientWrapper struct {
23102520 MockSubnets * mockconverged.MockSubnets [subnetModels.Subnet ]
23112521 MockVMs * mockconverged.MockVMs [vmmModels.Vm ]
23122522 MockTasks * mockconverged.MockTasks [prismModels.Task , prismErrors.AppMessage ]
2523+ MockVolumeGroups * mockconverged.MockVolumeGroups [volumesconfig.VolumeGroup , volumesconfig.VmAttachment ]
23132524}
23142525
23152526// NewMockConvergedClient creates a new mock converged client
@@ -2323,6 +2534,7 @@ func NewMockConvergedClient(ctrl *gomock.Controller) *MockConvergedClientWrapper
23232534 mockTasks := mockconverged.NewMockTasks [prismModels.Task , prismErrors.AppMessage ](ctrl )
23242535 // Create mock VMs service with the correct type
23252536 mockVMs := mockconverged.NewMockVMs [vmmModels.Vm ](ctrl )
2537+ mockVolumeGroups := mockconverged.NewMockVolumeGroups [volumesconfig.VolumeGroup , volumesconfig.VmAttachment ](ctrl )
23262538
23272539 realClient := & v4Converged.Client {
23282540 Client : converged.Client [
@@ -2337,6 +2549,8 @@ func NewMockConvergedClient(ctrl *gomock.Controller) *MockConvergedClientWrapper
23372549 vmmModels.Vm ,
23382550 prismModels.Task ,
23392551 prismErrors.AppMessage ,
2552+ volumesconfig.VolumeGroup ,
2553+ volumesconfig.VmAttachment ,
23402554 ]{
23412555 AntiAffinityPolicies : mockAntiAffinityPolicies ,
23422556 Clusters : mockClusters ,
@@ -2346,6 +2560,7 @@ func NewMockConvergedClient(ctrl *gomock.Controller) *MockConvergedClientWrapper
23462560 Subnets : mockSubnets ,
23472561 VMs : mockVMs ,
23482562 Tasks : mockTasks ,
2563+ VolumeGroups : mockVolumeGroups ,
23492564 },
23502565 }
23512566
@@ -2359,5 +2574,6 @@ func NewMockConvergedClient(ctrl *gomock.Controller) *MockConvergedClientWrapper
23592574 MockSubnets : mockSubnets ,
23602575 MockVMs : mockVMs ,
23612576 MockTasks : mockTasks ,
2577+ MockVolumeGroups : mockVolumeGroups ,
23622578 }
23632579}
0 commit comments