Skip to content

Commit 236d63a

Browse files
committed
smb restore from snapshot
1 parent 5cdcab0 commit 236d63a

File tree

10 files changed

+349
-19
lines changed

10 files changed

+349
-19
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
apiVersion: v1
3+
kind: PersistentVolumeClaim
4+
metadata:
5+
name: pvc-azurefile-snapshot-restored
6+
spec:
7+
accessModes:
8+
- ReadWriteMany
9+
storageClassName: azurefile-csi
10+
resources:
11+
requests:
12+
storage: 100Gi
13+
dataSource:
14+
name: azurefile-volume-snapshot
15+
kind: VolumeSnapshot
16+
apiGroup: snapshot.storage.k8s.io

pkg/azurefile/controllerserver.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"net/url"
23+
"os/exec"
2324
"strconv"
2425
"strings"
2526
"time"
@@ -718,7 +719,7 @@ func (d *Driver) copyVolume(ctx context.Context, req *csi.CreateVolumeRequest, a
718719
vs := req.VolumeContentSource
719720
switch vs.Type.(type) {
720721
case *csi.VolumeContentSource_Snapshot:
721-
return status.Errorf(codes.InvalidArgument, "copy volume from volumeSnapshot is not supported")
722+
return d.restoreSnapshot(ctx, req, accountKey, shareOptions, storageEndpointSuffix)
722723
case *csi.VolumeContentSource_Volume:
723724
return d.copyFileShare(ctx, req, accountKey, shareOptions, storageEndpointSuffix)
724725
default:
@@ -1072,6 +1073,65 @@ func (d *Driver) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsReques
10721073
return nil, status.Error(codes.Unimplemented, "")
10731074
}
10741075

1076+
// restoreSnapshot restores from a snapshot
1077+
func (d *Driver) restoreSnapshot(ctx context.Context, req *csi.CreateVolumeRequest, accountKey string, shareOptions *fileclient.ShareOptions, storageEndpointSuffix string) error {
1078+
if shareOptions.Protocol == storage.EnabledProtocolsNFS {
1079+
return fmt.Errorf("protocol nfs is not supported for snapshot restore")
1080+
}
1081+
var sourceSnapshotID string
1082+
if req.GetVolumeContentSource() != nil && req.GetVolumeContentSource().GetSnapshot() != nil {
1083+
sourceSnapshotID = req.GetVolumeContentSource().GetSnapshot().GetSnapshotId()
1084+
}
1085+
resourceGroupName, accountName, srcFileShareName, _, _, _, err := GetFileShareInfo(sourceSnapshotID) //nolint:dogsled
1086+
if err != nil {
1087+
return status.Error(codes.NotFound, err.Error())
1088+
}
1089+
dstFileShareName := shareOptions.Name
1090+
if srcFileShareName == "" || dstFileShareName == "" {
1091+
return fmt.Errorf("srcFileShareName(%s) or dstFileShareName(%s) is empty", srcFileShareName, dstFileShareName)
1092+
}
1093+
1094+
klog.V(2).Infof("generate sas token for account(%s)", accountName)
1095+
accountSasToken, genErr := generateSASToken(accountName, accountKey, storageEndpointSuffix, d.sasTokenExpirationMinutes)
1096+
if genErr != nil {
1097+
return genErr
1098+
}
1099+
1100+
timeAfter := time.After(waitForCopyTimeout)
1101+
timeTick := time.Tick(waitForCopyInterval)
1102+
srcPath := fmt.Sprintf("https://%s.file.%s/%s%s", accountName, storageEndpointSuffix, srcFileShareName, accountSasToken)
1103+
dstPath := fmt.Sprintf("https://%s.file.%s/%s%s", accountName, storageEndpointSuffix, dstFileShareName, accountSasToken)
1104+
1105+
jobState, percent, err := getAzcopyJob(dstFileShareName)
1106+
klog.V(2).Infof("azcopy job status: %s, copy percent: %s%%, error: %v", jobState, percent, err)
1107+
if jobState == AzcopyJobError || jobState == AzcopyJobCompleted {
1108+
return err
1109+
}
1110+
klog.V(2).Infof("begin to copy fileshare %s to %s", srcFileShareName, dstFileShareName)
1111+
for {
1112+
select {
1113+
case <-timeTick:
1114+
jobState, percent, err := getAzcopyJob(dstFileShareName)
1115+
klog.V(2).Infof("azcopy job status: %s, copy percent: %s%%, error: %v", jobState, percent, err)
1116+
switch jobState {
1117+
case AzcopyJobError, AzcopyJobCompleted:
1118+
return err
1119+
case AzcopyJobNotFound:
1120+
klog.V(2).Infof("copy fileshare %s to %s", srcFileShareName, dstFileShareName)
1121+
out, copyErr := exec.Command("azcopy", "copy", srcPath, dstPath, "--recursive", "--check-length=false").CombinedOutput()
1122+
if copyErr != nil {
1123+
klog.Warningf("CopyFileShare(%s, %s, %s) failed with error(%v): %v", resourceGroupName, accountName, dstFileShareName, copyErr, string(out))
1124+
} else {
1125+
klog.V(2).Infof("copied fileshare %s to %s successfully", srcFileShareName, dstFileShareName)
1126+
}
1127+
return copyErr
1128+
}
1129+
case <-timeAfter:
1130+
return fmt.Errorf("timeout waiting for copy fileshare %s to %s succeed", srcFileShareName, dstFileShareName)
1131+
}
1132+
}
1133+
}
1134+
10751135
// ControllerExpandVolume controller expand volume
10761136
func (d *Driver) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) {
10771137
volumeID := req.GetVolumeId()

pkg/azurefile/controllerserver_test.go

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,7 +1438,7 @@ func TestCopyVolume(t *testing.T) {
14381438
testFunc func(t *testing.T)
14391439
}{
14401440
{
1441-
name: "copy volume from volumeSnapshot is not supported",
1441+
name: "restore volume from volumeSnapshot nfs is not supported",
14421442
testFunc: func(t *testing.T) {
14431443
allParam := map[string]string{}
14441444

@@ -1463,8 +1463,74 @@ func TestCopyVolume(t *testing.T) {
14631463
d := NewFakeDriver()
14641464
ctx := context.Background()
14651465

1466-
expectedErr := status.Errorf(codes.InvalidArgument, "copy volume from volumeSnapshot is not supported")
1467-
err := d.copyVolume(ctx, req, "", nil, "core.windows.net")
1466+
expectedErr := fmt.Errorf("protocol nfs is not supported for snapshot restore")
1467+
err := d.copyVolume(ctx, req, "", &fileclient.ShareOptions{Protocol: storage.EnabledProtocolsNFS}, "core.windows.net")
1468+
if !reflect.DeepEqual(err, expectedErr) {
1469+
t.Errorf("Unexpected error: %v", err)
1470+
}
1471+
},
1472+
},
1473+
{
1474+
name: "restore volume from volumeSnapshot not found",
1475+
testFunc: func(t *testing.T) {
1476+
allParam := map[string]string{}
1477+
1478+
volumeSnapshotSource := &csi.VolumeContentSource_SnapshotSource{
1479+
SnapshotId: "unit-test",
1480+
}
1481+
volumeContentSourceSnapshotSource := &csi.VolumeContentSource_Snapshot{
1482+
Snapshot: volumeSnapshotSource,
1483+
}
1484+
volumecontensource := csi.VolumeContentSource{
1485+
Type: volumeContentSourceSnapshotSource,
1486+
}
1487+
1488+
req := &csi.CreateVolumeRequest{
1489+
Name: "random-vol-name-valid-request",
1490+
VolumeCapabilities: stdVolCap,
1491+
CapacityRange: lessThanPremCapRange,
1492+
Parameters: allParam,
1493+
VolumeContentSource: &volumecontensource,
1494+
}
1495+
1496+
d := NewFakeDriver()
1497+
ctx := context.Background()
1498+
1499+
expectedErr := status.Errorf(codes.NotFound, "error parsing volume id: \"unit-test\", should at least contain two #")
1500+
err := d.copyVolume(ctx, req, "", &fileclient.ShareOptions{Name: "dstFileshare"}, "core.windows.net")
1501+
if !reflect.DeepEqual(err, expectedErr) {
1502+
t.Errorf("Unexpected error: %v", err)
1503+
}
1504+
},
1505+
},
1506+
{
1507+
name: "restore volume from volumeSnapshot src fileshare is empty",
1508+
testFunc: func(t *testing.T) {
1509+
allParam := map[string]string{}
1510+
1511+
volumeSnapshotSource := &csi.VolumeContentSource_SnapshotSource{
1512+
SnapshotId: "rg#unit-test##",
1513+
}
1514+
volumeContentSourceSnapshotSource := &csi.VolumeContentSource_Snapshot{
1515+
Snapshot: volumeSnapshotSource,
1516+
}
1517+
volumecontensource := csi.VolumeContentSource{
1518+
Type: volumeContentSourceSnapshotSource,
1519+
}
1520+
1521+
req := &csi.CreateVolumeRequest{
1522+
Name: "random-vol-name-valid-request",
1523+
VolumeCapabilities: stdVolCap,
1524+
CapacityRange: lessThanPremCapRange,
1525+
Parameters: allParam,
1526+
VolumeContentSource: &volumecontensource,
1527+
}
1528+
1529+
d := NewFakeDriver()
1530+
ctx := context.Background()
1531+
1532+
expectedErr := fmt.Errorf("srcFileShareName() or dstFileShareName(dstFileshare) is empty")
1533+
err := d.copyVolume(ctx, req, "", &fileclient.ShareOptions{Name: "dstFileshare"}, "core.windows.net")
14681534
if !reflect.DeepEqual(err, expectedErr) {
14691535
t.Errorf("Unexpected error: %v", err)
14701536
}

test/e2e/driver/azurefile_driver.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ func (d *AzureFileDriver) GetPreProvisionStorageClass(parameters map[string]stri
7070
return getStorageClass(generateName, provisioner, parameters, mountOptions, reclaimPolicy, bindingMode, nil)
7171
}
7272

73-
func (d *AzureFileDriver) GetVolumeSnapshotClass(namespace string) *snapshotv1.VolumeSnapshotClass {
73+
func (d *AzureFileDriver) GetVolumeSnapshotClass(namespace string, parameters map[string]string) *snapshotv1.VolumeSnapshotClass {
7474
provisioner := d.driverName
7575
generateName := fmt.Sprintf("%s-%s-dynamic-sc-", namespace, normalizeProvisioner(provisioner))
76-
return getVolumeSnapshotClass(generateName, provisioner)
76+
return getVolumeSnapshotClass(generateName, provisioner, parameters)
7777
}
7878

7979
func (d *AzureFileDriver) GetPersistentVolume(volumeID string, fsType string, size string, reclaimPolicy *v1.PersistentVolumeReclaimPolicy, namespace string, attrib map[string]string, nodeStageSecretRef string) *v1.PersistentVolume {

test/e2e/driver/driver.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type PreProvisionedVolumeTestDriver interface {
4949
}
5050

5151
type VolumeSnapshotTestDriver interface {
52-
GetVolumeSnapshotClass(namespace string) *snapshotv1.VolumeSnapshotClass
52+
GetVolumeSnapshotClass(namespace string, parameters map[string]string) *snapshotv1.VolumeSnapshotClass
5353
}
5454

5555
func getStorageClass(
@@ -84,13 +84,13 @@ func getStorageClass(
8484
}
8585
}
8686

87-
func getVolumeSnapshotClass(generateName string, provisioner string) *snapshotv1.VolumeSnapshotClass {
87+
func getVolumeSnapshotClass(generateName string, provisioner string, parameters map[string]string) *snapshotv1.VolumeSnapshotClass {
8888
return &snapshotv1.VolumeSnapshotClass{
8989
TypeMeta: metav1.TypeMeta{
9090
Kind: VolumeSnapshotClassKind,
9191
APIVersion: SnapshotAPIVersion,
9292
},
93-
93+
Parameters: parameters,
9494
ObjectMeta: metav1.ObjectMeta{
9595
GenerateName: generateName,
9696
},

test/e2e/dynamic_provisioning_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,86 @@ var _ = ginkgo.Describe("Dynamic Provisioning", func() {
14761476
}
14771477
test.Run(ctx, cs, ns)
14781478
})
1479+
1480+
ginkgo.It("should create a pod, write and read to it, take a volume snapshot, and create another pod from the snapshot [disk.csi.azure.com]", func(ctx ginkgo.SpecContext) {
1481+
skipIfUsingInTreeVolumePlugin()
1482+
skipIfTestingInWindowsCluster()
1483+
1484+
pod := testsuites.PodDetails{
1485+
IsWindows: isWindowsCluster,
1486+
WinServerVer: winServerVer,
1487+
Cmd: "echo 'hello world' > /mnt/test-1/data",
1488+
Volumes: []testsuites.VolumeDetails{
1489+
{
1490+
ClaimSize: "10Gi",
1491+
VolumeMount: testsuites.VolumeMountDetails{
1492+
NameGenerate: "test-volume-",
1493+
MountPathGenerate: "/mnt/test-",
1494+
},
1495+
},
1496+
},
1497+
}
1498+
podWithSnapshot := testsuites.PodDetails{
1499+
IsWindows: isWindowsCluster,
1500+
WinServerVer: winServerVer,
1501+
Cmd: "grep 'hello world' /mnt/test-1/data",
1502+
}
1503+
test := testsuites.DynamicallyProvisionedVolumeSnapshotTest{
1504+
CSIDriver: testDriver,
1505+
Pod: pod,
1506+
PodWithSnapshot: podWithSnapshot,
1507+
StorageClassParameters: map[string]string{"skuName": "Standard_LRS"},
1508+
SnapshotStorageClassParameters: map[string]string{
1509+
"incremental": "false", "dataAccessAuthMode": "AzureActiveDirectory",
1510+
},
1511+
}
1512+
test.Run(ctx, cs, snapshotrcs, ns)
1513+
})
1514+
1515+
ginkgo.It("should create a pod, write to its pv, take a volume snapshot, overwrite data in original pv, create another pod from the snapshot, and read unaltered original data from original pv[disk.csi.azure.com]", func(ctx ginkgo.SpecContext) {
1516+
skipIfUsingInTreeVolumePlugin()
1517+
skipIfTestingInWindowsCluster()
1518+
1519+
pod := testsuites.PodDetails{
1520+
IsWindows: isWindowsCluster,
1521+
WinServerVer: winServerVer,
1522+
Cmd: "echo 'hello world' > /mnt/test-1/data",
1523+
Volumes: []testsuites.VolumeDetails{
1524+
{
1525+
ClaimSize: "10Gi",
1526+
VolumeMount: testsuites.VolumeMountDetails{
1527+
NameGenerate: "test-volume-",
1528+
MountPathGenerate: "/mnt/test-",
1529+
},
1530+
},
1531+
},
1532+
}
1533+
1534+
podOverwrite := testsuites.PodDetails{
1535+
IsWindows: isWindowsCluster,
1536+
WinServerVer: winServerVer,
1537+
Cmd: "echo 'overwrite' > /mnt/test-1/data; sleep 3600",
1538+
}
1539+
1540+
podWithSnapshot := testsuites.PodDetails{
1541+
IsWindows: isWindowsCluster,
1542+
WinServerVer: winServerVer,
1543+
Cmd: "grep 'hello world' /mnt/test-1/data",
1544+
}
1545+
1546+
test := testsuites.DynamicallyProvisionedVolumeSnapshotTest{
1547+
CSIDriver: testDriver,
1548+
Pod: pod,
1549+
ShouldOverwrite: true,
1550+
PodOverwrite: podOverwrite,
1551+
PodWithSnapshot: podWithSnapshot,
1552+
StorageClassParameters: map[string]string{"skuName": "Standard_LRS"},
1553+
SnapshotStorageClassParameters: map[string]string{
1554+
"incremental": "true", "dataAccessAuthMode": "None",
1555+
},
1556+
}
1557+
test.Run(ctx, cs, snapshotrcs, ns)
1558+
})
14791559
})
14801560

14811561
func restClient(group string, version string) (restclientset.Interface, error) {

0 commit comments

Comments
 (0)