Skip to content

Commit 9115cfb

Browse files
authored
Merge pull request #825 from pjsharath28/volume-clone
Add support for volume clone operations through CSI driver
2 parents 8fd1cc1 + e2d045c commit 9115cfb

File tree

6 files changed

+277
-6
lines changed

6 files changed

+277
-6
lines changed

pkg/cloud/cloud_interface.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ type Cloud interface {
2626
AttachDisk(volumeID string, nodeID string) (err error)
2727
DetachDisk(volumeID string, nodeID string) (err error)
2828
ResizeDisk(volumeID string, reqSize int64) (newSize int64, err error)
29+
CloneDisk(sourceVolumeName string, cloneVolumeName string) (disk *Disk, err error)
2930
WaitForVolumeState(volumeID, state string) error
31+
WaitForCloneStatus(taskId string) error
3032
GetDiskByName(name string) (disk *Disk, err error)
33+
GetDiskByNamePrefix(namePrefix string) (disk *Disk, err error)
3134
GetDiskByID(volumeID string) (disk *Disk, err error)
3235
GetPVMInstanceByName(instanceName string) (instance *PVMInstance, err error)
3336
GetPVMInstanceByID(instanceID string) (instance *PVMInstance, err error)

pkg/cloud/mocks/mock_cloud.go

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cloud/powervs.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const (
5151
type powerVSCloud struct {
5252
pvmInstancesClient *instance.IBMPIInstanceClient
5353
volClient *instance.IBMPIVolumeClient
54+
cloneVolumeClient *instance.IBMPICloneVolumeClient
5455
}
5556

5657
type PVMInstance struct {
@@ -96,10 +97,12 @@ func newPowerVSCloud(cloudInstanceID, zone string, debug bool) (Cloud, error) {
9697
backgroundContext := context.Background()
9798
volClient := instance.NewIBMPIVolumeClient(backgroundContext, piSession, cloudInstanceID)
9899
pvmInstancesClient := instance.NewIBMPIInstanceClient(backgroundContext, piSession, cloudInstanceID)
100+
cloneVolumeClient := instance.NewIBMPICloneVolumeClient(backgroundContext, piSession, cloudInstanceID)
99101

100102
return &powerVSCloud{
101103
pvmInstancesClient: pvmInstancesClient,
102104
volClient: volClient,
105+
cloneVolumeClient: cloneVolumeClient,
103106
}, nil
104107
}
105108

@@ -209,6 +212,39 @@ func (p *powerVSCloud) ResizeDisk(volumeID string, reqSize int64) (newSize int64
209212
return int64(*v.Size), nil
210213
}
211214

215+
func (p *powerVSCloud) CloneDisk(sourceVolumeID string, cloneVolumeName string) (disk *Disk, err error) {
216+
_, err = p.GetDiskByID(sourceVolumeID)
217+
if err != nil {
218+
return nil, err
219+
}
220+
cloneVolumeReq := &models.VolumesCloneAsyncRequest{
221+
Name: &cloneVolumeName,
222+
VolumeIDs: []string{sourceVolumeID},
223+
}
224+
cloneTaskRef, err := p.cloneVolumeClient.Create(cloneVolumeReq)
225+
if err != nil {
226+
return nil, err
227+
}
228+
cloneTaskId := cloneTaskRef.CloneTaskID
229+
err = p.WaitForCloneStatus(*cloneTaskId)
230+
if err != nil {
231+
return nil, err
232+
}
233+
clonedVolumeDetails, err := p.cloneVolumeClient.Get(*cloneTaskId)
234+
if err != nil {
235+
return nil, err
236+
}
237+
if clonedVolumeDetails == nil || len(clonedVolumeDetails.ClonedVolumes) == 0 {
238+
return nil, errors.New("cloned volume not found")
239+
}
240+
clonedVolumeID := clonedVolumeDetails.ClonedVolumes[0].ClonedVolumeID
241+
err = p.WaitForVolumeState(clonedVolumeID, VolumeAvailableState)
242+
if err != nil {
243+
return nil, err
244+
}
245+
return p.GetDiskByID(clonedVolumeID)
246+
}
247+
212248
func (p *powerVSCloud) WaitForVolumeState(volumeID, state string) error {
213249
ctx := context.Background()
214250
return wait.PollUntilContextTimeout(ctx, PollInterval, PollTimeout, true, func(ctx context.Context) (bool, error) {
@@ -221,6 +257,18 @@ func (p *powerVSCloud) WaitForVolumeState(volumeID, state string) error {
221257
})
222258
}
223259

260+
func (p *powerVSCloud) WaitForCloneStatus(cloneTaskId string) error {
261+
ctx := context.Background()
262+
return wait.PollUntilContextTimeout(ctx, PollInterval, PollTimeout, true, func(ctx context.Context) (bool, error) {
263+
c, err := p.cloneVolumeClient.Get(cloneTaskId)
264+
if err != nil {
265+
return false, err
266+
}
267+
spew.Dump(*c)
268+
return *c.Status == "completed", nil
269+
})
270+
}
271+
224272
func (p *powerVSCloud) GetDiskByName(name string) (disk *Disk, err error) {
225273
vols, err := p.volClient.GetAll()
226274
if err != nil {
@@ -242,6 +290,27 @@ func (p *powerVSCloud) GetDiskByName(name string) (disk *Disk, err error) {
242290
return nil, ErrNotFound
243291
}
244292

293+
func (p *powerVSCloud) GetDiskByNamePrefix(namePrefix string) (disk *Disk, err error) {
294+
vols, err := p.volClient.GetAll()
295+
if err != nil {
296+
return nil, err
297+
}
298+
for _, v := range vols.Volumes {
299+
if strings.HasPrefix(*v.Name, namePrefix) {
300+
return &Disk{
301+
Name: *v.Name,
302+
DiskType: *v.DiskType,
303+
VolumeID: *v.VolumeID,
304+
WWN: strings.ToLower(*v.Wwn),
305+
Shareable: *v.Shareable,
306+
CapacityGiB: int64(*v.Size),
307+
}, nil
308+
}
309+
}
310+
311+
return nil, ErrNotFound
312+
}
313+
245314
func (p *powerVSCloud) GetDiskByID(volumeID string) (disk *Disk, err error) {
246315
v, err := p.volClient.Get(volumeID)
247316
if err != nil {

pkg/driver/controller.go

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ var (
4545
csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
4646
csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME,
4747
csi.ControllerServiceCapability_RPC_EXPAND_VOLUME,
48+
csi.ControllerServiceCapability_RPC_CLONE_VOLUME,
4849
}
4950
)
5051

@@ -173,6 +174,10 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol
173174
VolumeType: volumeType,
174175
}
175176

177+
if req.GetVolumeContentSource() != nil {
178+
return handleClone(d.cloud, req, volName, volSizeBytes, opts)
179+
}
180+
176181
// check if disk exists
177182
// disk exists only if previous createVolume request fails due to any network/tcp error
178183
diskDetails, _ := d.cloud.GetDiskByName(volName)
@@ -186,14 +191,14 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol
186191
if err != nil {
187192
return nil, status.Errorf(codes.Internal, "Disk already exists and not in expected state")
188193
}
189-
return newCreateVolumeResponse(diskDetails), nil
194+
return newCreateVolumeResponse(diskDetails, req.VolumeContentSource), nil
190195
}
191196

192197
disk, err := d.cloud.CreateDisk(volName, opts)
193198
if err != nil {
194199
return nil, status.Errorf(codes.Internal, "Could not create volume %q: %v", volName, err)
195200
}
196-
return newCreateVolumeResponse(disk), nil
201+
return newCreateVolumeResponse(disk, req.VolumeContentSource), nil
197202
}
198203

199204
func (d *controllerService) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
@@ -455,9 +460,7 @@ func (d *controllerService) ListSnapshots(ctx context.Context, req *csi.ListSnap
455460
return nil, status.Error(codes.Unimplemented, "")
456461
}
457462

458-
func newCreateVolumeResponse(disk *cloud.Disk) *csi.CreateVolumeResponse {
459-
var src *csi.VolumeContentSource
460-
463+
func newCreateVolumeResponse(disk *cloud.Disk, src *csi.VolumeContentSource) *csi.CreateVolumeResponse {
461464
return &csi.CreateVolumeResponse{
462465
Volume: &csi.Volume{
463466
VolumeId: disk.VolumeID,
@@ -496,3 +499,42 @@ func verifyVolumeDetails(payload *cloud.DiskOptions, diskDetails *cloud.Disk) er
496499
}
497500
return nil
498501
}
502+
503+
func handleClone(cloud cloud.Cloud, req *csi.CreateVolumeRequest, volName string, volSizeBytes int64, opts *cloud.DiskOptions) (*csi.CreateVolumeResponse, error) {
504+
volumeSource := req.VolumeContentSource
505+
switch volumeSource.Type.(type) {
506+
case *csi.VolumeContentSource_Volume:
507+
diskDetails, _ := cloud.GetDiskByNamePrefix("clone-" + req.GetName())
508+
if diskDetails != nil {
509+
err := verifyVolumeDetails(opts, diskDetails)
510+
if err != nil {
511+
return nil, err
512+
}
513+
return newCreateVolumeResponse(diskDetails, req.VolumeContentSource), nil
514+
}
515+
if srcVolume := volumeSource.GetVolume(); srcVolume != nil {
516+
srcVolumeID := srcVolume.GetVolumeId()
517+
diskDetails, err := cloud.GetDiskByID(srcVolumeID)
518+
if err != nil {
519+
return nil, status.Errorf(codes.Internal, "Could not get the source volume %q: %v", srcVolumeID, err)
520+
}
521+
if util.GiBToBytes(diskDetails.CapacityGiB) != volSizeBytes {
522+
return nil, status.Errorf(codes.Internal, "Cannot clone volume %v, source volume size is not equal to the clone volume", srcVolumeID)
523+
}
524+
err = verifyVolumeDetails(opts, diskDetails)
525+
if err != nil {
526+
return nil, err
527+
}
528+
diskFromSourceVolume, err := cloud.CloneDisk(srcVolumeID, volName)
529+
if err != nil {
530+
return nil, status.Errorf(codes.Internal, "Could not clone volume %q: %v", volName, err)
531+
}
532+
cloneDiskDetails, err := cloud.GetDiskByID(diskFromSourceVolume.VolumeID)
533+
if err != nil {
534+
return nil, status.Errorf(codes.Internal, "Could not get volume %q after clone: %v", volName, err)
535+
}
536+
return newCreateVolumeResponse(cloneDiskDetails, req.VolumeContentSource), nil
537+
}
538+
}
539+
return nil, status.Errorf(codes.InvalidArgument, "%v not a proper volume source", volumeSource)
540+
}

pkg/driver/controller_test.go

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ import (
1919
"reflect"
2020
"testing"
2121

22-
csi "github.com/container-storage-interface/spec/lib/go/csi"
22+
"github.com/container-storage-interface/spec/lib/go/csi"
2323
"go.uber.org/mock/gomock"
2424
"google.golang.org/grpc/codes"
2525
"google.golang.org/grpc/status"
26+
2627
"sigs.k8s.io/ibm-powervs-block-csi-driver/pkg/cloud"
2728
mocks "sigs.k8s.io/ibm-powervs-block-csi-driver/pkg/cloud/mocks"
2829
"sigs.k8s.io/ibm-powervs-block-csi-driver/pkg/util"
@@ -95,7 +96,106 @@ func TestCreateVolume(t *testing.T) {
9596
}
9697
},
9798
},
99+
{
100+
name: "success normal with datasource PVC",
101+
testFunc: func(t *testing.T) {
102+
req := &csi.CreateVolumeRequest{
103+
Name: "clone-volume-name",
104+
CapacityRange: stdCapRange,
105+
VolumeCapabilities: stdVolCap,
106+
Parameters: stdParams,
107+
VolumeContentSource: &csi.VolumeContentSource{
108+
Type: &csi.VolumeContentSource_Volume{
109+
Volume: &csi.VolumeContentSource_VolumeSource{
110+
VolumeId: "test-volume-src-100",
111+
},
112+
},
113+
},
114+
}
115+
116+
ctx := context.Background()
117+
118+
mockDisk := &cloud.Disk{
119+
VolumeID: req.Name,
120+
CapacityGiB: util.BytesToGiB(stdVolSize),
121+
DiskType: cloud.DefaultVolumeType,
122+
}
123+
mockSrcDisk := &cloud.Disk{
124+
VolumeID: "test-volume-src-100",
125+
CapacityGiB: util.BytesToGiB(stdVolSize),
126+
DiskType: cloud.DefaultVolumeType,
127+
}
128+
129+
mockCtl := gomock.NewController(t)
130+
defer mockCtl.Finish()
131+
132+
mockCloud := mocks.NewMockCloud(mockCtl)
133+
mockCloud.EXPECT().GetDiskByNamePrefix(gomock.Eq("clone-"+req.Name)).Return(nil, nil)
134+
mockCloud.EXPECT().GetDiskByID(gomock.Eq(mockSrcDisk.VolumeID)).Return(mockSrcDisk, nil)
135+
mockCloud.EXPECT().CloneDisk(gomock.Eq(mockSrcDisk.VolumeID), gomock.Eq(req.Name)).Return(mockDisk, nil)
136+
mockCloud.EXPECT().GetDiskByID(gomock.Eq(mockDisk.VolumeID)).Return(mockDisk, nil)
137+
138+
powervsDriver := controllerService{
139+
cloud: mockCloud,
140+
driverOptions: &Options{},
141+
volumeLocks: util.NewVolumeLocks(),
142+
}
143+
144+
if _, err := powervsDriver.CreateVolume(ctx, req); err != nil {
145+
srvErr, ok := status.FromError(err)
146+
if !ok {
147+
t.Fatalf("Could not get error status code from error: %v", srvErr)
148+
}
149+
t.Fatalf("Unexpected error: %v", srvErr.Code())
150+
}
151+
},
152+
},
153+
{
154+
name: "Create PVC with Data source - volume already exists",
155+
testFunc: func(t *testing.T) {
156+
req := &csi.CreateVolumeRequest{
157+
Name: "clone-volume-name",
158+
CapacityRange: &csi.CapacityRange{RequiredBytes: stdVolSize},
159+
VolumeCapabilities: stdVolCap,
160+
Parameters: stdParams,
161+
VolumeContentSource: &csi.VolumeContentSource{
162+
Type: &csi.VolumeContentSource_Volume{
163+
Volume: &csi.VolumeContentSource_VolumeSource{
164+
VolumeId: "test-volume-src-100",
165+
},
166+
},
167+
},
168+
}
169+
170+
ctx := context.Background()
171+
172+
mockDisk := &cloud.Disk{
173+
VolumeID: req.Name,
174+
CapacityGiB: util.BytesToGiB(stdVolSize),
175+
DiskType: cloud.DefaultVolumeType,
176+
}
98177

178+
mockCtl := gomock.NewController(t)
179+
defer mockCtl.Finish()
180+
181+
mockCloud := mocks.NewMockCloud(mockCtl)
182+
mockCloud.EXPECT().GetDiskByNamePrefix(gomock.Eq("clone-"+req.Name)).Return(mockDisk, nil)
183+
184+
powervsDriver := controllerService{
185+
cloud: mockCloud,
186+
driverOptions: &Options{},
187+
volumeLocks: util.NewVolumeLocks(),
188+
}
189+
190+
if _, err := powervsDriver.CreateVolume(ctx, req); err != nil {
191+
srvErr, ok := status.FromError(err)
192+
if !ok {
193+
t.Fatalf("Could not get error status code from error: %v", srvErr)
194+
}
195+
t.Fatalf("Unexpected error: %v", srvErr.Code())
196+
}
197+
},
198+
},
99199
{
100200
name: "csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER",
101201
testFunc: func(t *testing.T) {

0 commit comments

Comments
 (0)