@@ -19,12 +19,18 @@ package blob
19
19
import (
20
20
"context"
21
21
"fmt"
22
+ "net/url"
23
+ "os/exec"
22
24
"strconv"
23
25
"strings"
26
+ "time"
24
27
25
28
"google.golang.org/grpc/codes"
26
29
"google.golang.org/grpc/status"
27
30
31
+ "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
32
+ "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas"
33
+ "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/service"
28
34
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2021-09-01/storage"
29
35
azstorage "github.com/Azure/azure-sdk-for-go/storage"
30
36
"github.com/container-storage-interface/spec/lib/go/csi"
@@ -42,6 +48,9 @@ import (
42
48
43
49
const (
44
50
privateEndpoint = "privateendpoint"
51
+
52
+ waitForCopyInterval = 5 * time .Second
53
+ waitForCopyTimeout = 3 * time .Minute
45
54
)
46
55
47
56
// CreateVolume provisions a volume
@@ -61,6 +70,11 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest)
61
70
}
62
71
63
72
if acquired := d .volumeLocks .TryAcquire (volName ); ! acquired {
73
+ // logging the job status if it's volume cloning
74
+ if req .GetVolumeContentSource () != nil {
75
+ jobState , percent , err := d .azcopy .GetAzcopyJob (volName )
76
+ klog .V (2 ).Infof ("azcopy job status: %s, copy percent: %s%%, error: %v" , jobState , percent , err )
77
+ }
64
78
return nil , status .Errorf (codes .Aborted , volumeOperationAlreadyExistsFmt , volName )
65
79
}
66
80
defer d .volumeLocks .Release (volName )
@@ -313,7 +327,16 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest)
313
327
}
314
328
315
329
var volumeID string
316
- mc := metrics .NewMetricContext (blobCSIDriverName , "controller_create_volume" , d .cloud .ResourceGroup , d .cloud .SubscriptionID , d .Name )
330
+ requestName := "controller_create_volume"
331
+ if req .GetVolumeContentSource () != nil {
332
+ switch req .VolumeContentSource .Type .(type ) {
333
+ case * csi.VolumeContentSource_Snapshot :
334
+ requestName = "controller_create_volume_from_snapshot"
335
+ case * csi.VolumeContentSource_Volume :
336
+ requestName = "controller_create_volume_from_volume"
337
+ }
338
+ }
339
+ mc := metrics .NewMetricContext (blobCSIDriverName , requestName , d .cloud .ResourceGroup , d .cloud .SubscriptionID , d .Name )
317
340
isOperationSucceeded := false
318
341
defer func () {
319
342
mc .ObserveOperationWithResult (isOperationSucceeded , VolumeID , volumeID )
@@ -387,9 +410,20 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest)
387
410
setKeyValueInMap (parameters , containerNameField , validContainerName )
388
411
}
389
412
390
- klog .V (2 ).Infof ("begin to create container(%s) on account(%s) type(%s) subsID(%s) rg(%s) location(%s) size(%d)" , validContainerName , accountName , storageAccountType , subsID , resourceGroup , location , requestGiB )
391
- if err := d .CreateBlobContainer (ctx , subsID , resourceGroup , accountName , validContainerName , secrets ); err != nil {
392
- return nil , status .Errorf (codes .Internal , "failed to create container(%s) on account(%s) type(%s) rg(%s) location(%s) size(%d), error: %v" , validContainerName , accountName , storageAccountType , resourceGroup , location , requestGiB , err )
413
+ if req .GetVolumeContentSource () != nil {
414
+ if accountKey == "" {
415
+ if _ , accountKey , err = d .GetStorageAccesskey (ctx , accountOptions , secrets , secretName , secretNamespace ); err != nil {
416
+ return nil , status .Errorf (codes .Internal , "failed to GetStorageAccesskey on account(%s) rg(%s), error: %v" , accountOptions .Name , accountOptions .ResourceGroup , err )
417
+ }
418
+ }
419
+ if err := d .copyVolume (ctx , req , accountKey , validContainerName , storageEndpointSuffix ); err != nil {
420
+ return nil , err
421
+ }
422
+ } else {
423
+ klog .V (2 ).Infof ("begin to create container(%s) on account(%s) type(%s) subsID(%s) rg(%s) location(%s) size(%d)" , validContainerName , accountName , storageAccountType , subsID , resourceGroup , location , requestGiB )
424
+ if err := d .CreateBlobContainer (ctx , subsID , resourceGroup , accountName , validContainerName , secrets ); err != nil {
425
+ return nil , status .Errorf (codes .Internal , "failed to create container(%s) on account(%s) type(%s) rg(%s) location(%s) size(%d), error: %v" , validContainerName , accountName , storageAccountType , resourceGroup , location , requestGiB , err )
426
+ }
393
427
}
394
428
395
429
if storeAccountKey && len (req .GetSecrets ()) == 0 {
@@ -430,6 +464,7 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest)
430
464
VolumeId : volumeID ,
431
465
CapacityBytes : req .GetCapacityRange ().GetRequiredBytes (),
432
466
VolumeContext : parameters ,
467
+ ContentSource : req .GetVolumeContentSource (),
433
468
},
434
469
}, nil
435
470
}
@@ -675,6 +710,75 @@ func (d *Driver) DeleteBlobContainer(ctx context.Context, subsID, resourceGroupN
675
710
})
676
711
}
677
712
713
+ // CopyBlobContainer copies a blob container in the same storage account
714
+ func (d * Driver ) copyBlobContainer (ctx context.Context , req * csi.CreateVolumeRequest , accountKey , dstContainerName , storageEndpointSuffix string ) error {
715
+ var sourceVolumeID string
716
+ if req .GetVolumeContentSource () != nil && req .GetVolumeContentSource ().GetVolume () != nil {
717
+ sourceVolumeID = req .GetVolumeContentSource ().GetVolume ().GetVolumeId ()
718
+
719
+ }
720
+ resourceGroupName , accountName , srcContainerName , _ , _ , err := GetContainerInfo (sourceVolumeID ) //nolint:dogsled
721
+ if err != nil {
722
+ return status .Error (codes .NotFound , err .Error ())
723
+ }
724
+ if srcContainerName == "" || dstContainerName == "" {
725
+ return fmt .Errorf ("srcContainerName(%s) or dstContainerName(%s) is empty" , srcContainerName , dstContainerName )
726
+ }
727
+
728
+ klog .V (2 ).Infof ("generate sas token for account(%s)" , accountName )
729
+ accountSasToken , genErr := generateSASToken (accountName , accountKey , storageEndpointSuffix , d .sasTokenExpirationMinutes )
730
+ if genErr != nil {
731
+ return genErr
732
+ }
733
+
734
+ timeAfter := time .After (waitForCopyTimeout )
735
+ timeTick := time .Tick (waitForCopyInterval )
736
+ srcPath := fmt .Sprintf ("https://%s.blob.%s/%s%s" , accountName , storageEndpointSuffix , srcContainerName , accountSasToken )
737
+ dstPath := fmt .Sprintf ("https://%s.blob.%s/%s%s" , accountName , storageEndpointSuffix , dstContainerName , accountSasToken )
738
+
739
+ jobState , percent , err := d .azcopy .GetAzcopyJob (dstContainerName )
740
+ klog .V (2 ).Infof ("azcopy job status: %s, copy percent: %s%%, error: %v" , jobState , percent , err )
741
+ if jobState == util .AzcopyJobError || jobState == util .AzcopyJobCompleted {
742
+ return err
743
+ }
744
+ klog .V (2 ).Infof ("begin to copy blob container %s to %s" , srcContainerName , dstContainerName )
745
+ for {
746
+ select {
747
+ case <- timeTick :
748
+ jobState , percent , err := d .azcopy .GetAzcopyJob (dstContainerName )
749
+ klog .V (2 ).Infof ("azcopy job status: %s, copy percent: %s%%, error: %v" , jobState , percent , err )
750
+ switch jobState {
751
+ case util .AzcopyJobError , util .AzcopyJobCompleted :
752
+ return err
753
+ case util .AzcopyJobNotFound :
754
+ klog .V (2 ).Infof ("copy blob container %s to %s" , srcContainerName , dstContainerName )
755
+ out , copyErr := exec .Command ("azcopy" , "copy" , srcPath , dstPath , "--recursive" , "--check-length=false" ).CombinedOutput ()
756
+ if copyErr != nil {
757
+ klog .Warningf ("CopyBlobContainer(%s, %s, %s) failed with error(%v): %v" , resourceGroupName , accountName , dstPath , copyErr , string (out ))
758
+ } else {
759
+ klog .V (2 ).Infof ("copied blob container %s to %s successfully" , srcContainerName , dstContainerName )
760
+ }
761
+ return copyErr
762
+ }
763
+ case <- timeAfter :
764
+ return fmt .Errorf ("timeout waiting for copy blob container %s to %s succeed" , srcContainerName , dstContainerName )
765
+ }
766
+ }
767
+ }
768
+
769
+ // copyVolume copies a volume form volume or snapshot, snapshot is not supported now
770
+ func (d * Driver ) copyVolume (ctx context.Context , req * csi.CreateVolumeRequest , accountKey , dstContainerName , storageEndpointSuffix string ) error {
771
+ vs := req .VolumeContentSource
772
+ switch vs .Type .(type ) {
773
+ case * csi.VolumeContentSource_Snapshot :
774
+ return status .Errorf (codes .InvalidArgument , "copy volume from volumeSnapshot is not supported" )
775
+ case * csi.VolumeContentSource_Volume :
776
+ return d .copyBlobContainer (ctx , req , accountKey , dstContainerName , storageEndpointSuffix )
777
+ default :
778
+ return status .Errorf (codes .InvalidArgument , "%v is not a proper volume source" , vs )
779
+ }
780
+ }
781
+
678
782
// isValidVolumeCapabilities validates the given VolumeCapability array is valid
679
783
func isValidVolumeCapabilities (volCaps []* csi.VolumeCapability ) error {
680
784
if len (volCaps ) == 0 {
@@ -699,3 +803,27 @@ func parseDays(dayStr string) (int32, error) {
699
803
700
804
return int32 (days ), nil
701
805
}
806
+
807
+ // generateSASToken generate a sas token for storage account
808
+ func generateSASToken (accountName , accountKey , storageEndpointSuffix string , expiryTime int ) (string , error ) {
809
+ credential , err := azblob .NewSharedKeyCredential (accountName , accountKey )
810
+ if err != nil {
811
+ return "" , status .Errorf (codes .Internal , fmt .Sprintf ("failed to generate sas token in creating new shared key credential, accountName: %s, err: %s" , accountName , err .Error ()))
812
+ }
813
+ serviceClient , err := service .NewClientWithSharedKeyCredential (fmt .Sprintf ("https://%s.blob.%s/" , accountName , storageEndpointSuffix ), credential , nil )
814
+ if err != nil {
815
+ return "" , status .Errorf (codes .Internal , fmt .Sprintf ("failed to generate sas token in creating new client with shared key credential, accountName: %s, err: %s" , accountName , err .Error ()))
816
+ }
817
+ sasURL , err := serviceClient .GetSASURL (
818
+ sas.AccountResourceTypes {Object : true , Service : false , Container : true },
819
+ sas.AccountPermissions {Read : true , List : true , Write : true },
820
+ sas.AccountServices {Blob : true }, time .Now (), time .Now ().Add (time .Duration (expiryTime )* time .Minute ))
821
+ if err != nil {
822
+ return "" , err
823
+ }
824
+ u , err := url .Parse (sasURL )
825
+ if err != nil {
826
+ return "" , err
827
+ }
828
+ return "?" + u .RawQuery , nil
829
+ }
0 commit comments