Skip to content

Commit 739333b

Browse files
committed
Add support for Volume snapshot for CloudStack CSI driver
1 parent fad82ed commit 739333b

File tree

11 files changed

+301
-1
lines changed

11 files changed

+301
-1
lines changed

deploy/k8s/controller-deployment.yaml

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,45 @@ spec:
178178
type: RuntimeDefault
179179
readOnlyRootFilesystem: true
180180
allowPrivilegeEscalation: false
181-
181+
- name: csi-snapshotter
182+
image: registry.k8s.io/sig-storage/csi-snapshotter:v6.3.0
183+
args:
184+
- "--v=5"
185+
- "--csi-address=$(CSI_ADDRESS)"
186+
- "--leader-election"
187+
- "--leader-election-lease-duration=30s"
188+
- "--leader-election-renew-deadline=20s"
189+
- "--leader-election-retry-period=10s"
190+
env:
191+
- name: CSI_ADDRESS
192+
value: /var/lib/csi/sockets/pluginproxy/csi.sock
193+
volumeMounts:
194+
- name: socket-dir
195+
mountPath: /var/lib/csi/sockets/pluginproxy/
196+
resources:
197+
limits:
198+
cpu: 400m
199+
memory: 200Mi
200+
requests:
201+
cpu: 10m
202+
memory: 20Mi
203+
- name: snapshot-controller
204+
image: registry.k8s.io/sig-storage/snapshot-controller:v6.3.0
205+
args:
206+
- "--v=5"
207+
- "--leader-election"
208+
- "--leader-election-lease-duration=30s"
209+
- "--leader-election-renew-deadline=20s"
210+
- "--leader-election-retry-period=10s"
211+
resources:
212+
limits:
213+
cpu: 400m
214+
memory: 200Mi
215+
requests:
216+
cpu: 10m
217+
memory: 20Mi
182218
- name: liveness-probe
219+
imagePullPolicy: IfNotPresent
183220
image: registry.k8s.io/sig-storage/livenessprobe:v2.12.0
184221
args:
185222
- "--v=4"

deploy/k8s/rbac.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ rules:
3636
- apiGroups: ["storage.k8s.io"]
3737
resources: ["volumeattachments/status"]
3838
verbs: ["patch"]
39+
- apiGroups: ["snapshot.storage.k8s.io"]
40+
resources: ["volumesnapshots", "volumesnapshots/status", "volumesnapshotclasses", "volumesnapshotcontents", "volumesnapshotcontents/status"]
41+
verbs: ["get", "list", "watch", "update", "create", "patch"]
3942
---
4043
apiVersion: rbac.authorization.k8s.io/v1
4144
kind: ClusterRoleBinding

deploy/k8s/volume-snapshot-class.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: snapshot.storage.k8s.io/v1
2+
kind: VolumeSnapshotClass
3+
metadata:
4+
name: cloudstack-snapshot
5+
driver: csi.cloudstack.apache.org
6+
deletionPolicy: Delete
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: v1
2+
kind: PersistentVolumeClaim
3+
metadata:
4+
name: snapshot-pvc-1
5+
spec:
6+
accessModes:
7+
- ReadWriteOnce
8+
resources:
9+
requests:
10+
storage: 10Gi
11+
dataSource:
12+
name: snapshot-1
13+
kind: VolumeSnapshot
14+
apiGroup: snapshot.storage.k8s.io
15+
storageClassName: cloudstack-custom

examples/k8s/snapshot/pvc.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: v1
2+
kind: PersistentVolumeClaim
3+
metadata:
4+
name: my-pvc
5+
spec:
6+
accessModes:
7+
- ReadWriteOnce
8+
resources:
9+
requests:
10+
storage: 10Gi
11+
storageClassName: cloudstack-custom

examples/k8s/snapshot/snapshot.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: snapshot.storage.k8s.io/v1
2+
kind: VolumeSnapshot
3+
metadata:
4+
name: snapshot-1
5+
spec:
6+
volumeSnapshotClassName: cloudstack-snapshot
7+
source:
8+
persistentVolumeClaimName: my-pvc

pkg/cloud/cloud.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ type Interface interface {
2323
AttachVolume(ctx context.Context, volumeID, vmID string) (string, error)
2424
DetachVolume(ctx context.Context, volumeID string) error
2525
ExpandVolume(ctx context.Context, volumeID string, newSizeInGB int64) error
26+
27+
CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (string, error)
28+
CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error)
29+
DeleteSnapshot(ctx context.Context, snapshotID string) error
2630
}
2731

2832
// Volume represents a CloudStack volume.
@@ -34,12 +38,26 @@ type Volume struct {
3438
Size int64
3539

3640
DiskOfferingID string
41+
DomainID string
42+
ProjectID string
3743
ZoneID string
3844

3945
VirtualMachineID string
4046
DeviceID string
4147
}
4248

49+
type Snapshot struct {
50+
ID string
51+
Name string
52+
53+
DomainID string
54+
ProjectID string
55+
ZoneID string
56+
57+
VolumeID string
58+
CreatedAt string
59+
}
60+
4361
// VM represents a CloudStack Virtual Machine.
4462
type VM struct {
4563
ID string

pkg/cloud/fake/fake.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const zoneID = "a1887604-237c-4212-a9cd-94620b7880fa"
1515

1616
type fakeConnector struct {
1717
node *cloud.VM
18+
snapshot *cloud.Snapshot
1819
volumesByID map[string]cloud.Volume
1920
volumesByName map[string]cloud.Volume
2021
}
@@ -36,8 +37,18 @@ func New() cloud.Interface {
3637
ZoneID: zoneID,
3738
}
3839

40+
snapshot := &cloud.Snapshot{
41+
ID: "9d076136-657b-4c84-b279-455da3ea484c",
42+
Name: "pvc-vol-snap-1",
43+
DomainID: "51f0fcb5-db16-4637-94f5-30131010214f",
44+
ZoneID: "bdab539f-651e-431a-979d-5d3c48b54fcf",
45+
VolumeID: "4f1f610d-6f17-4ff9-9228-e4062af93e54",
46+
CreatedAt: "2025-07-07 16:13:06",
47+
}
48+
3949
return &fakeConnector{
4050
node: node,
51+
snapshot: snapshot,
4152
volumesByID: map[string]cloud.Volume{volume.ID: volume},
4253
volumesByName: map[string]cloud.Volume{volume.Name: volume},
4354
}
@@ -124,3 +135,15 @@ func (f *fakeConnector) ExpandVolume(_ context.Context, volumeID string, newSize
124135

125136
return cloud.ErrNotFound
126137
}
138+
139+
func (f *fakeConnector) CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (string, error) {
140+
return "1", nil
141+
}
142+
143+
func (f *fakeConnector) CreateSnapshot(ctx context.Context, volumeID string) (*cloud.Snapshot, error) {
144+
return f.snapshot, nil
145+
}
146+
147+
func (f *fakeConnector) DeleteSnapshot(ctx context.Context, snapshotID string) error {
148+
return nil
149+
}

pkg/cloud/snapshots.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package cloud
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
"github.com/apache/cloudstack-go/v2/cloudstack"
8+
"google.golang.org/grpc/codes"
9+
"google.golang.org/grpc/status"
10+
)
11+
12+
func (c *client) ListSnapshots(p *cloudstack.ListSnapshotsParams) (*Snapshot, error) {
13+
l, err := c.Snapshot.ListSnapshots(p)
14+
if err != nil {
15+
return nil, err
16+
}
17+
if l.Count == 0 {
18+
return nil, ErrNotFound
19+
}
20+
if l.Count > 1 {
21+
return nil, ErrTooManyResults
22+
}
23+
snapshot := l.Snapshots[0]
24+
s := Snapshot{
25+
ID: snapshot.Id,
26+
Name: snapshot.Name,
27+
DomainID: snapshot.Domainid,
28+
ProjectID: snapshot.Projectid,
29+
ZoneID: snapshot.Zoneid,
30+
VolumeID: snapshot.Volumeid,
31+
}
32+
33+
return &s, nil
34+
}
35+
36+
func (c *client) CreateSnapshot(ctx context.Context, volumeID string) (*Snapshot, error) {
37+
p := c.Snapshot.NewCreateSnapshotParams(volumeID)
38+
snapshot, err := c.Snapshot.CreateSnapshot(p)
39+
if err != nil {
40+
return nil, status.Errorf(codes.Internal, "Error %v", err)
41+
}
42+
43+
snap := Snapshot{
44+
ID: snapshot.Id,
45+
Name: snapshot.Name,
46+
DomainID: snapshot.Domainid,
47+
ProjectID: snapshot.Projectid,
48+
ZoneID: snapshot.Zoneid,
49+
VolumeID: snapshot.Volumeid,
50+
CreatedAt: snapshot.Created,
51+
}
52+
return &snap, nil
53+
}
54+
55+
func (c *client) DeleteSnapshot(ctx context.Context, snapshotID string) error {
56+
p := c.Snapshot.NewDeleteSnapshotParams(snapshotID)
57+
_, err := c.Snapshot.DeleteSnapshot(p)
58+
if err != nil && strings.Contains(err.Error(), "4350") {
59+
// CloudStack error InvalidParameterValueException
60+
return ErrNotFound
61+
}
62+
63+
return err
64+
}

pkg/cloud/volumes.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ func (c *client) listVolumes(p *cloudstack.ListVolumesParams) (*Volume, error) {
2929
Name: vol.Name,
3030
Size: vol.Size,
3131
DiskOfferingID: vol.Diskofferingid,
32+
DomainID: vol.Domainid,
33+
ProjectID: vol.Projectid,
3234
ZoneID: vol.Zoneid,
3335
VirtualMachineID: vol.Virtualmachineid,
3436
DeviceID: strconv.FormatInt(vol.Deviceid, 10),
@@ -153,3 +155,34 @@ func (c *client) ExpandVolume(ctx context.Context, volumeID string, newSizeInGB
153155

154156
return nil
155157
}
158+
159+
func (c *client) CreateVolumeFromSnapshot(ctx context.Context, diskOfferingID, zoneID, name, domainID, projectID, snapshotID string, sizeInGB int64) (string, error) {
160+
logger := klog.FromContext(ctx)
161+
snapshot, _, err := c.Snapshot.GetSnapshotByID(snapshotID)
162+
if err != nil {
163+
return "", fmt.Errorf("failed to retrieve snapshot '%s': %w", snapshotID, err)
164+
}
165+
166+
p := c.Volume.NewCreateVolumeParams()
167+
p.SetDiskofferingid(diskOfferingID)
168+
p.SetZoneid(zoneID)
169+
if projectID != "" {
170+
p.SetProjectid(projectID)
171+
}
172+
p.SetName(name)
173+
p.SetSnapshotid(snapshot.Id)
174+
175+
logger.V(2).Info("CloudStack API call", "command", "CreateVolume", "params", map[string]string{
176+
"name": name,
177+
"snapshotid": snapshotID,
178+
"projectid": projectID,
179+
})
180+
// Execute the API call to create volume from snapshot
181+
vol, err := c.Volume.CreateVolume(p)
182+
if err != nil {
183+
// Handle the error accordingly
184+
return "", fmt.Errorf("failed to create volume from snapshot'%s': %w", snapshotID, err)
185+
}
186+
187+
return vol.Id, err
188+
}

0 commit comments

Comments
 (0)