Skip to content

Commit ce66bf8

Browse files
committed
feat: implement volume snapshots
1 parent aeb0a8d commit ce66bf8

File tree

2 files changed

+291
-8
lines changed

2 files changed

+291
-8
lines changed

pkg/nfs/controllerserver.go

Lines changed: 290 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package nfs
1818

1919
import (
2020
"fmt"
21+
"io/fs"
2122
"os"
2223
"os/exec"
2324
"path/filepath"
@@ -29,6 +30,7 @@ import (
2930
"golang.org/x/net/context"
3031
"google.golang.org/grpc/codes"
3132
"google.golang.org/grpc/status"
33+
"google.golang.org/protobuf/types/known/timestamppb"
3234

3335
"k8s.io/klog/v2"
3436
)
@@ -59,6 +61,35 @@ type nfsVolume struct {
5961
onDelete string
6062
}
6163

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+
6293
// Ordering of elements in the CSI volume id.
6394
// ID is of the form {server}/{baseDir}/{subDir}.
6495
// TODO: This volume id format limits baseDir and
@@ -74,6 +105,19 @@ const (
74105
totalIDElements // Always last
75106
)
76107

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+
77121
// CreateVolume create a volume
78122
func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
79123
name := req.GetName()
@@ -263,11 +307,115 @@ func (cs *ControllerServer) ControllerGetCapabilities(ctx context.Context, req *
263307
}
264308

265309
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
267376
}
268377

269378
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
271419
}
272420

273421
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)
325473
return err
326474
}
327475

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+
328517
func (cs *ControllerServer) copyFromVolume(ctx context.Context, req *csi.CreateVolumeRequest, dstVol *nfsVolume) error {
329518
srcVol, err := getNfsVolFromID(req.GetVolumeContentSource().GetVolume().GetVolumeId())
330519
if err != nil {
@@ -340,26 +529,26 @@ func (cs *ControllerServer) copyFromVolume(ctx context.Context, req *csi.CreateV
340529
volCap = req.GetVolumeCapabilities()[0]
341530
}
342531
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)
344533
}
345534
defer func() {
346535
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)
348537
}
349538
}()
350539
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)
352541
}
353542
defer func() {
354543
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)
356545
}
357546
}()
358547

359548
// recursive 'cp' with '-a' to handle symlinks
360549
out, err := exec.Command("cp", "-a", srcPath, dstPath).CombinedOutput()
361550
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))
363552
}
364553
klog.V(2).Infof("copied %s -> %s", srcPath, dstPath)
365554
return nil
@@ -369,14 +558,48 @@ func (cs *ControllerServer) copyVolume(ctx context.Context, req *csi.CreateVolum
369558
vs := req.VolumeContentSource
370559
switch vs.Type.(type) {
371560
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)
373562
case *csi.VolumeContentSource_Volume:
374563
return cs.copyFromVolume(ctx, req, vol)
375564
default:
376565
return status.Errorf(codes.InvalidArgument, "%v not a proper volume source", vs)
377566
}
378567
}
379568

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+
380603
// newNFSVolume Convert VolumeCreate parameters to an nfsVolume
381604
func newNFSVolume(name string, size int64, params map[string]string, defaultOnDeletePolicy string) (*nfsVolume, error) {
382605
var server, baseDir, subDir, onDelete string
@@ -470,6 +693,17 @@ func getVolumeIDFromNfsVol(vol *nfsVolume) string {
470693
return strings.Join(idElements, separator)
471694
}
472695

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+
473707
// Given a CSI volume id, return a nfsVolume
474708
// sample volume Id:
475709
//
@@ -513,6 +747,25 @@ func getNfsVolFromID(id string) (*nfsVolume, error) {
513747
}, nil
514748
}
515749

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+
516769
// isValidVolumeCapabilities validates the given VolumeCapability array is valid
517770
func isValidVolumeCapabilities(volCaps []*csi.VolumeCapability) error {
518771
if len(volCaps) == 0 {
@@ -525,3 +778,32 @@ func isValidVolumeCapabilities(volCaps []*csi.VolumeCapability) error {
525778
}
526779
return nil
527780
}
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

Comments
 (0)