@@ -18,6 +18,7 @@ package nfs
18
18
19
19
import (
20
20
"fmt"
21
+ "io/fs"
21
22
"os"
22
23
"os/exec"
23
24
"path/filepath"
@@ -29,6 +30,7 @@ import (
29
30
"golang.org/x/net/context"
30
31
"google.golang.org/grpc/codes"
31
32
"google.golang.org/grpc/status"
33
+ "google.golang.org/protobuf/types/known/timestamppb"
32
34
33
35
"k8s.io/klog/v2"
34
36
)
@@ -59,6 +61,35 @@ type nfsVolume struct {
59
61
onDelete string
60
62
}
61
63
64
+ // nfsSnapshot is an internal representation of a volume snapshot
65
+ // created by the provisioner.
66
+ type nfsSnapshot struct {
67
+ // Snapshot id.
68
+ id string
69
+ // Address of the NFS server.
70
+ // Matches paramServer.
71
+ server string
72
+ // Base directory of the NFS server to create snapshots under
73
+ // Matches paramShare.
74
+ baseDir string
75
+ // Snapshot name.
76
+ uuid string
77
+ // Source volume.
78
+ src string
79
+ }
80
+
81
+ func (snap nfsSnapshot ) archiveSubPath () string {
82
+ return snap .uuid
83
+ }
84
+
85
+ func (snap nfsSnapshot ) archiveName () string {
86
+ return fmt .Sprintf ("%v.tar.gz" , snap .src )
87
+ }
88
+
89
+ func (snap nfsSnapshot ) archivePath () string {
90
+ return filepath .Join (snap .archiveSubPath (), snap .archiveName ())
91
+ }
92
+
62
93
// Ordering of elements in the CSI volume id.
63
94
// ID is of the form {server}/{baseDir}/{subDir}.
64
95
// TODO: This volume id format limits baseDir and
@@ -74,6 +105,19 @@ const (
74
105
totalIDElements // Always last
75
106
)
76
107
108
+ // Ordering of elements in the CSI snapshot id.
109
+ // ID is of the form {server}/{baseDir}/{snapName}/{srcVolumeName}.
110
+ // Adding a new element should always go at the end
111
+ // before totalSnapIDElements
112
+ const (
113
+ idSnapServer = iota
114
+ idSnapBaseDir
115
+ idSnapUUID
116
+ idSnapArchivePath
117
+ idSnapArchiveName
118
+ totalIDSnapElements // Always last
119
+ )
120
+
77
121
// CreateVolume create a volume
78
122
func (cs * ControllerServer ) CreateVolume (ctx context.Context , req * csi.CreateVolumeRequest ) (* csi.CreateVolumeResponse , error ) {
79
123
name := req .GetName ()
@@ -263,11 +307,115 @@ func (cs *ControllerServer) ControllerGetCapabilities(ctx context.Context, req *
263
307
}
264
308
265
309
func (cs * ControllerServer ) CreateSnapshot (ctx context.Context , req * csi.CreateSnapshotRequest ) (* csi.CreateSnapshotResponse , error ) {
266
- return nil , status .Error (codes .Unimplemented , "" )
310
+ if len (req .GetName ()) == 0 {
311
+ return nil , status .Error (codes .InvalidArgument , "CreateSnapshot name must be provided" )
312
+ }
313
+ if len (req .GetSourceVolumeId ()) == 0 {
314
+ return nil , status .Error (codes .InvalidArgument , "CreateSnapshot source volume ID must be provided" )
315
+ }
316
+
317
+ srcVol , err := getNfsVolFromID (req .GetSourceVolumeId ())
318
+ if err != nil {
319
+ return nil , status .Errorf (codes .NotFound , "failed to create source volume: %v" , err )
320
+ }
321
+ snapshot , err := newNFSSnapshot (req .GetName (), req .GetParameters (), srcVol )
322
+ if err != nil {
323
+ return nil , status .Errorf (codes .NotFound , "failed to create nfsSnapshot: %v" , err )
324
+ }
325
+ snapVol := volumeFromSnapshot (snapshot )
326
+ if err = cs .internalMount (ctx , snapVol , nil , nil ); err != nil {
327
+ return nil , status .Errorf (codes .Internal , "failed to mount snapshot nfs server: %v" , err )
328
+ }
329
+ defer func () {
330
+ if err = cs .internalUnmount (ctx , snapVol ); err != nil {
331
+ klog .Warningf ("failed to unmount snapshot nfs server: %v" , err )
332
+ }
333
+ }()
334
+ snapInternalVolPath := filepath .Join (getInternalVolumePath (cs .Driver .workingMountDir , snapVol ), snapshot .archiveSubPath ())
335
+ if err = os .MkdirAll (snapInternalVolPath , 0777 ); err != nil {
336
+ return nil , status .Errorf (codes .Internal , "failed to make subdirectory: %v" , err )
337
+ }
338
+ if err := validateSnapshot (snapInternalVolPath , snapshot ); err != nil {
339
+ return nil , err
340
+ }
341
+
342
+ if err = cs .internalMount (ctx , srcVol , nil , nil ); err != nil {
343
+ return nil , status .Errorf (codes .Internal , "failed to mount src nfs server: %v" , err )
344
+ }
345
+ defer func () {
346
+ if err = cs .internalUnmount (ctx , srcVol ); err != nil {
347
+ klog .Warningf ("failed to unmount src nfs server: %v" , err )
348
+ }
349
+ }()
350
+
351
+ srcPath := getInternalVolumePath (cs .Driver .workingMountDir , srcVol )
352
+ dstPath := filepath .Join (snapInternalVolPath , snapshot .archiveName ())
353
+ klog .V (2 ).Infof ("archiving %v -> %v" , srcPath , dstPath )
354
+ out , err := exec .Command ("tar" , "-C" , srcPath , "-czvf" , dstPath , "." ).CombinedOutput ()
355
+ if err != nil {
356
+ return nil , status .Errorf (codes .Internal , "failed to create archive for snapshot: %v: %v" , err , string (out ))
357
+ }
358
+ klog .V (2 ).Infof ("archived %s -> %s" , srcPath , dstPath )
359
+
360
+ var snapshotSize int64
361
+ fi , err := os .Stat (dstPath )
362
+ if err != nil {
363
+ klog .Warningf ("failed to determine snapshot size: %v" , err )
364
+ } else {
365
+ snapshotSize = fi .Size ()
366
+ }
367
+ return & csi.CreateSnapshotResponse {
368
+ Snapshot : & csi.Snapshot {
369
+ SnapshotId : snapshot .id ,
370
+ SourceVolumeId : srcVol .id ,
371
+ SizeBytes : snapshotSize ,
372
+ CreationTime : timestamppb .Now (),
373
+ ReadyToUse : true ,
374
+ },
375
+ }, nil
267
376
}
268
377
269
378
func (cs * ControllerServer ) DeleteSnapshot (ctx context.Context , req * csi.DeleteSnapshotRequest ) (* csi.DeleteSnapshotResponse , error ) {
270
- return nil , status .Error (codes .Unimplemented , "" )
379
+ if len (req .GetSnapshotId ()) == 0 {
380
+ return nil , status .Error (codes .InvalidArgument , "Snapshot ID is required for deletion" )
381
+ }
382
+ snap , err := getNfsSnapFromID (req .GetSnapshotId ())
383
+ if err != nil {
384
+ // An invalid ID should be treated as doesn't exist
385
+ klog .Warningf ("failed to get nfs snapshot for id %v deletion: %v" , req .GetSnapshotId (), err )
386
+ return & csi.DeleteSnapshotResponse {}, nil
387
+ }
388
+
389
+ var volCap * csi.VolumeCapability
390
+ mountOptions := getMountOptions (req .GetSecrets ())
391
+ if mountOptions != "" {
392
+ klog .V (2 ).Infof ("DeleteSnapshot: found mountOptions(%s) for snapshot(%s)" , mountOptions , req .GetSnapshotId ())
393
+ volCap = & csi.VolumeCapability {
394
+ AccessType : & csi.VolumeCapability_Mount {
395
+ Mount : & csi.VolumeCapability_MountVolume {
396
+ MountFlags : []string {mountOptions },
397
+ },
398
+ },
399
+ }
400
+ }
401
+ vol := volumeFromSnapshot (snap )
402
+ if err = cs .internalMount (ctx , vol , nil , volCap ); err != nil {
403
+ return nil , status .Errorf (codes .Internal , "failed to mount nfs server for snapshot deletion: %v" , err )
404
+ }
405
+ defer func () {
406
+ if err = cs .internalUnmount (ctx , vol ); err != nil {
407
+ klog .Warningf ("failed to unmount nfs server after snapshot deletion: %v" , err )
408
+ }
409
+ }()
410
+
411
+ // delete snapshot archive
412
+ internalVolumePath := filepath .Join (getInternalVolumePath (cs .Driver .workingMountDir , vol ), snap .archiveSubPath ())
413
+ klog .V (2 ).Infof ("Removing snapshot archive at %v" , internalVolumePath )
414
+ if err = os .RemoveAll (internalVolumePath ); err != nil {
415
+ return nil , status .Errorf (codes .Internal , "failed to delete subdirectory: %v" , err .Error ())
416
+ }
417
+
418
+ return & csi.DeleteSnapshotResponse {}, nil
271
419
}
272
420
273
421
func (cs * ControllerServer ) ListSnapshots (ctx context.Context , req * csi.ListSnapshotsRequest ) (* csi.ListSnapshotsResponse , error ) {
@@ -325,6 +473,47 @@ func (cs *ControllerServer) internalUnmount(ctx context.Context, vol *nfsVolume)
325
473
return err
326
474
}
327
475
476
+ func (cs * ControllerServer ) copyFromSnapshot (ctx context.Context , req * csi.CreateVolumeRequest , dstVol * nfsVolume ) error {
477
+ snap , err := getNfsSnapFromID (req .VolumeContentSource .GetSnapshot ().GetSnapshotId ())
478
+ if err != nil {
479
+ return status .Error (codes .NotFound , err .Error ())
480
+ }
481
+ snapVol := volumeFromSnapshot (snap )
482
+
483
+ var volCap * csi.VolumeCapability
484
+ if len (req .GetVolumeCapabilities ()) > 0 {
485
+ volCap = req .GetVolumeCapabilities ()[0 ]
486
+ }
487
+
488
+ if err = cs .internalMount (ctx , snapVol , nil , volCap ); err != nil {
489
+ return status .Errorf (codes .Internal , "failed to mount src nfs server for snapshot volume copy: %v" , err )
490
+ }
491
+ defer func () {
492
+ if err = cs .internalUnmount (ctx , snapVol ); err != nil {
493
+ klog .Warningf ("failed to unmount src nfs server after snapshot volume copy: %v" , err )
494
+ }
495
+ }()
496
+ if err = cs .internalMount (ctx , dstVol , nil , volCap ); err != nil {
497
+ return status .Errorf (codes .Internal , "failed to mount dst nfs server for snapshot volume copy: %v" , err )
498
+ }
499
+ defer func () {
500
+ if err = cs .internalUnmount (ctx , dstVol ); err != nil {
501
+ klog .Warningf ("failed to unmount dst nfs server after snapshot volume copy: %v" , err )
502
+ }
503
+ }()
504
+
505
+ // untar snapshot archive to dst path
506
+ snapPath := filepath .Join (getInternalVolumePath (cs .Driver .workingMountDir , snapVol ), snap .archivePath ())
507
+ dstPath := getInternalVolumePath (cs .Driver .workingMountDir , dstVol )
508
+ klog .V (2 ).Infof ("copy volume from snapshot %v -> %v" , snapPath , dstPath )
509
+ out , err := exec .Command ("tar" , "-xzvf" , snapPath , "-C" , dstPath ).CombinedOutput ()
510
+ if err != nil {
511
+ return status .Errorf (codes .Internal , "failed to copy volume for snapshot: %v: %v" , err , string (out ))
512
+ }
513
+ klog .V (2 ).Infof ("volume copied from snapshot %v -> %v" , snapPath , dstPath )
514
+ return nil
515
+ }
516
+
328
517
func (cs * ControllerServer ) copyFromVolume (ctx context.Context , req * csi.CreateVolumeRequest , dstVol * nfsVolume ) error {
329
518
srcVol , err := getNfsVolFromID (req .GetVolumeContentSource ().GetVolume ().GetVolumeId ())
330
519
if err != nil {
@@ -340,26 +529,26 @@ func (cs *ControllerServer) copyFromVolume(ctx context.Context, req *csi.CreateV
340
529
volCap = req .GetVolumeCapabilities ()[0 ]
341
530
}
342
531
if err = cs .internalMount (ctx , srcVol , nil , volCap ); err != nil {
343
- return status .Errorf (codes .Internal , "failed to mount src nfs server: %v" , err . Error () )
532
+ return status .Errorf (codes .Internal , "failed to mount src nfs server: %v" , err )
344
533
}
345
534
defer func () {
346
535
if err = cs .internalUnmount (ctx , srcVol ); err != nil {
347
- klog .Warningf ("failed to unmount nfs server: %v" , err . Error () )
536
+ klog .Warningf ("failed to unmount nfs server: %v" , err )
348
537
}
349
538
}()
350
539
if err = cs .internalMount (ctx , dstVol , nil , volCap ); err != nil {
351
- return status .Errorf (codes .Internal , "failed to mount dst nfs server: %v" , err . Error () )
540
+ return status .Errorf (codes .Internal , "failed to mount dst nfs server: %v" , err )
352
541
}
353
542
defer func () {
354
543
if err = cs .internalUnmount (ctx , dstVol ); err != nil {
355
- klog .Warningf ("failed to unmount dst nfs server: %v" , err . Error () )
544
+ klog .Warningf ("failed to unmount dst nfs server: %v" , err )
356
545
}
357
546
}()
358
547
359
548
// recursive 'cp' with '-a' to handle symlinks
360
549
out , err := exec .Command ("cp" , "-a" , srcPath , dstPath ).CombinedOutput ()
361
550
if err != nil {
362
- return status .Error (codes .Internal , fmt . Sprintf ( " %v: %v" , err , string (out ) ))
551
+ return status .Errorf (codes .Internal , "failed to copy volume %v: %v" , err , string (out ))
363
552
}
364
553
klog .V (2 ).Infof ("copied %s -> %s" , srcPath , dstPath )
365
554
return nil
@@ -369,14 +558,48 @@ func (cs *ControllerServer) copyVolume(ctx context.Context, req *csi.CreateVolum
369
558
vs := req .VolumeContentSource
370
559
switch vs .Type .(type ) {
371
560
case * csi.VolumeContentSource_Snapshot :
372
- return status . Error ( codes . Unimplemented , "Currently only volume copy from another volume is supported" )
561
+ return cs . copyFromSnapshot ( ctx , req , vol )
373
562
case * csi.VolumeContentSource_Volume :
374
563
return cs .copyFromVolume (ctx , req , vol )
375
564
default :
376
565
return status .Errorf (codes .InvalidArgument , "%v not a proper volume source" , vs )
377
566
}
378
567
}
379
568
569
+ // newNFSSnapshot Convert VolumeSnapshot parameters to a nfsSnapshot
570
+ func newNFSSnapshot (name string , params map [string ]string , vol * nfsVolume ) (* nfsSnapshot , error ) {
571
+ server := vol .server
572
+ baseDir := vol .baseDir
573
+ for k , v := range params {
574
+ switch strings .ToLower (k ) {
575
+ case paramServer :
576
+ server = v
577
+ case paramShare :
578
+ baseDir = v
579
+ }
580
+ }
581
+
582
+ if server == "" {
583
+ return nil , fmt .Errorf ("%v is a required parameter" , paramServer )
584
+ }
585
+ snapshot := & nfsSnapshot {
586
+ server : server ,
587
+ baseDir : baseDir ,
588
+ uuid : name ,
589
+ }
590
+ if vol .subDir != "" {
591
+ snapshot .src = vol .subDir
592
+ }
593
+ if vol .uuid != "" {
594
+ snapshot .src = vol .uuid
595
+ }
596
+ if snapshot .src == "" {
597
+ return nil , fmt .Errorf ("missing required source volume name" )
598
+ }
599
+ snapshot .id = getSnapshotIDFromNfsSnapshot (snapshot )
600
+ return snapshot , nil
601
+ }
602
+
380
603
// newNFSVolume Convert VolumeCreate parameters to an nfsVolume
381
604
func newNFSVolume (name string , size int64 , params map [string ]string , defaultOnDeletePolicy string ) (* nfsVolume , error ) {
382
605
var server , baseDir , subDir , onDelete string
@@ -470,6 +693,17 @@ func getVolumeIDFromNfsVol(vol *nfsVolume) string {
470
693
return strings .Join (idElements , separator )
471
694
}
472
695
696
+ // Given a nfsSnapshot, return a CSI snapshot id.
697
+ func getSnapshotIDFromNfsSnapshot (snap * nfsSnapshot ) string {
698
+ idElements := make ([]string , totalIDSnapElements )
699
+ idElements [idSnapServer ] = strings .Trim (snap .server , "/" )
700
+ idElements [idSnapBaseDir ] = strings .Trim (snap .baseDir , "/" )
701
+ idElements [idSnapUUID ] = snap .uuid
702
+ idElements [idSnapArchivePath ] = snap .uuid
703
+ idElements [idSnapArchiveName ] = snap .src
704
+ return strings .Join (idElements , separator )
705
+ }
706
+
473
707
// Given a CSI volume id, return a nfsVolume
474
708
// sample volume Id:
475
709
//
@@ -513,6 +747,25 @@ func getNfsVolFromID(id string) (*nfsVolume, error) {
513
747
}, nil
514
748
}
515
749
750
+ // Given a CSI snapshot ID, return a nfsSnapshot
751
+ // sample snapshot ID:
752
+ //
753
+ // nfs-server.default.svc.cluster.local#share#snapshot-016f784f-56f4-44d1-9041-5f59e82dbce1#snapshot-016f784f-56f4-44d1-9041-5f59e82dbce1#pvc-4bcbf944-b6f7-4bd0-b50f-3c3dd00efc64
754
+ func getNfsSnapFromID (id string ) (* nfsSnapshot , error ) {
755
+ segments := strings .Split (id , separator )
756
+ if len (segments ) == totalIDSnapElements {
757
+ return & nfsSnapshot {
758
+ id : id ,
759
+ server : segments [idSnapServer ],
760
+ baseDir : segments [idSnapBaseDir ],
761
+ src : segments [idSnapArchiveName ],
762
+ uuid : segments [idSnapUUID ],
763
+ }, nil
764
+ }
765
+
766
+ return & nfsSnapshot {}, fmt .Errorf ("failed to create nfsSnapshot from snapshot ID" )
767
+ }
768
+
516
769
// isValidVolumeCapabilities validates the given VolumeCapability array is valid
517
770
func isValidVolumeCapabilities (volCaps []* csi.VolumeCapability ) error {
518
771
if len (volCaps ) == 0 {
@@ -525,3 +778,32 @@ func isValidVolumeCapabilities(volCaps []*csi.VolumeCapability) error {
525
778
}
526
779
return nil
527
780
}
781
+
782
+ // Validate snapshot after internal mount
783
+ func validateSnapshot (snapInternalVolPath string , snap * nfsSnapshot ) error {
784
+ return filepath .WalkDir (snapInternalVolPath , func (path string , d fs.DirEntry , err error ) error {
785
+ if path == snapInternalVolPath {
786
+ // skip root
787
+ return nil
788
+ }
789
+ if err != nil {
790
+ return err
791
+ }
792
+ if d .Name () != snap .archiveName () {
793
+ // there should be just one archive in the snapshot path and archive name should match
794
+ return status .Errorf (codes .AlreadyExists , "snapshot with the same name but different source volume ID already exists: found %q, desired %q" , d .Name (), snap .archiveName ())
795
+ }
796
+ return nil
797
+ })
798
+ }
799
+
800
+ // Volume for snapshot internal mount/unmount
801
+ func volumeFromSnapshot (snap * nfsSnapshot ) * nfsVolume {
802
+ return & nfsVolume {
803
+ id : snap .id ,
804
+ server : snap .server ,
805
+ baseDir : snap .baseDir ,
806
+ subDir : snap .baseDir ,
807
+ uuid : snap .uuid ,
808
+ }
809
+ }
0 commit comments