Skip to content

Commit

Permalink
support deletion of snapshots after restore from backup
Browse files Browse the repository at this point in the history
When a user requests a restore from an off-site backup, they might want the
local snapshot to be deleted after the restore.

We already have this option when creating the snapshot by using a special
parameter on the snapshot class. However, we do not have that information
available directly.

To support this, we need to lookup the snapshot content and snapshot class
ourselfes, then parse the parameters. This is all optional, as LINSTOR CSI
still needs to work outside kubernetes, or inside Kubernetes with incomplete
information.

Signed-off-by: Moritz "WanzenBug" Wanzenböck <moritz.wanzenboeck@linbit.com>
  • Loading branch information
WanzenBug committed Oct 6, 2023
1 parent 6dbf0f2 commit 4ed2026
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Support to delete downloaded backups after restore operation (k8s only).

## [1.2.3] - 2023-08-31

### Changed
Expand Down
1 change: 1 addition & 0 deletions cmd/linstor-csi/linstor-csi.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ func main() {
driver.Expander(linstorClient),
driver.NodeInformer(linstorClient),
driver.TopologyPrefix(*propNs),
driver.ConfigureKubernetesIfAvailable(),
)
if err != nil {
log.Fatal(err)
Expand Down
16 changes: 15 additions & 1 deletion pkg/client/linstor.go
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,7 @@ func (s *Linstor) SnapDelete(ctx context.Context, snap *volume.Snapshot) error {
}

// VolFromSnap creates the volume using the data contained within the snapshot.
func (s *Linstor) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *volume.Info, params *volume.Parameters, topologies *csi.TopologyRequirement) error {
func (s *Linstor) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *volume.Info, params *volume.Parameters, snapParams *volume.SnapshotParameters, topologies *csi.TopologyRequirement) error {
logger := s.log.WithFields(logrus.Fields{
"volume": fmt.Sprintf("%+v", vol),
"snapshot": fmt.Sprintf("%+v", snap),
Expand Down Expand Up @@ -987,6 +987,20 @@ func (s *Linstor) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *v
return err
}

if snap.Remote != "" && snapParams != nil && snapParams.DeleteLocal {
logger.Info("deleting local copy of backup")

err := s.client.Resources.DeleteSnapshot(ctx, snap.SourceVolumeId, snap.SnapshotId)
if err != nil {
logger.WithError(err).Warn("deleting local copy of backup failed")
}

err = s.deleteResourceDefinitionAndGroupIfUnused(ctx, snap.GetSourceVolumeId())
if err != nil {
logger.WithError(err).Warn("deleting local RD of backup failed")
}
}

logger.Debug("success")
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/client/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func (s *MockStorage) FindSnapsBySource(ctx context.Context, sourceVol *volume.I
return results[start:end], nil
}

func (s *MockStorage) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *volume.Info, parameters *volume.Parameters, topologies *csi.TopologyRequirement) error {
func (s *MockStorage) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *volume.Info, parameters *volume.Parameters, snapParams *volume.SnapshotParameters, topologies *csi.TopologyRequirement) error {
s.createdVolumes = append(s.createdVolumes, vol)
return nil
}
Expand Down
86 changes: 84 additions & 2 deletions pkg/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"

"github.com/piraeusdatastore/linstor-csi/pkg/client"
"github.com/piraeusdatastore/linstor-csi/pkg/linstor"
Expand All @@ -57,6 +62,7 @@ type Driver struct {
VolumeStatter volume.VolumeStatter
Expander volume.Expander
NodeInformer volume.NodeInformer
kubeClient dynamic.Interface
srv *grpc.Server
log *logrus.Entry
version string
Expand Down Expand Up @@ -236,6 +242,20 @@ func LogLevel(s string) func(*Driver) error {
}
}

func ConfigureKubernetesIfAvailable() func(*Driver) error {
return func(d *Driver) error {
cfg, err := rest.InClusterConfig()
if err != nil {
// Not running in kubernetes
return nil
}

d.kubeClient, err = dynamic.NewForConfig(cfg)

return err
}
}

// GetPluginInfo https://github.com/container-storage-interface/spec/blob/v1.6.0/spec.md#getplugininfo
func (d Driver) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
return &csi.GetPluginInfoResponse{
Expand Down Expand Up @@ -1259,7 +1279,14 @@ func (d Driver) createNewVolume(ctx context.Context, info *volume.Info, params *
"CreateVolume failed for %s: snapshot not found in storage backend", req.GetName())
}

if err := d.Snapshots.VolFromSnap(ctx, snap, info, params, req.GetAccessibilityRequirements()); err != nil {
snapParams, err := d.maybeGetSnapshotParameters(ctx, snap)
if err != nil {
logger.WithError(err).Warn("failed to fetch snapshot parameters, continuing without it")

snapParams = nil
}

if err := d.Snapshots.VolFromSnap(ctx, snap, info, params, snapParams, req.GetAccessibilityRequirements()); err != nil {
d.failpathDelete(ctx, info.ID)
return nil, status.Errorf(codes.Internal,
"CreateVolume failed for %s: %v", req.GetName(), err)
Expand Down Expand Up @@ -1301,7 +1328,7 @@ func (d Driver) createNewVolume(ctx context.Context, info *volume.Info, params *
}
}()

err = d.Snapshots.VolFromSnap(ctx, snap, info, params, req.GetAccessibilityRequirements())
err = d.Snapshots.VolFromSnap(ctx, snap, info, params, nil, req.GetAccessibilityRequirements())
if err != nil {
d.failpathDelete(ctx, info.ID)

Expand Down Expand Up @@ -1345,6 +1372,61 @@ func (d Driver) createNewVolume(ctx context.Context, info *volume.Info, params *
}, nil
}

func findMatchingSnapshotClassName(snap *volume.Snapshot, contents ...unstructured.Unstructured) string {
for i := range contents {
content := contents[i].Object
if driver, _, _ := unstructured.NestedString(content, "spec", "driver"); driver != linstor.DriverName {
continue
}

if handle, _, _ := unstructured.NestedString(content, "status", "snapshotHandle"); handle != snap.SnapshotId {
continue
}

if readyToUse, _, _ := unstructured.NestedBool(content, "status", "readyToUse"); !readyToUse {
continue
}

snapshotClass, _, _ := unstructured.NestedString(content, "spec", "volumeSnapshotClassName")

return snapshotClass
}

return ""
}

func (d Driver) maybeGetSnapshotParameters(ctx context.Context, snap *volume.Snapshot) (*volume.SnapshotParameters, error) {
if d.kubeClient == nil {
return nil, nil
}

gv := schema.GroupVersion{Group: "snapshot.storage.k8s.io", Version: "v1"}
contentGvr := gv.WithResource("volumesnapshotcontents")
classGvr := gv.WithResource("volumesnapshotclasses")

result, err := d.kubeClient.Resource(contentGvr).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to fetch list of snapshot contents")
}

snapshotClassName := findMatchingSnapshotClassName(snap, result.Items...)
if snapshotClassName == "" {
return nil, fmt.Errorf("failed to determine snapshot class name")
}

class, err := d.kubeClient.Resource(classGvr).Get(ctx, snapshotClassName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to fetch snapshot class: %w", err)
}

rawParams, _, err := unstructured.NestedStringMap(class.Object, "parameters")
if err != nil {
return nil, fmt.Errorf("failed to parse snapshot class: %w", err)
}

return volume.NewSnapshotParameters(rawParams, nil)
}

// maybeDeleteLocalSnapshot deletes the local portion of a snapshot according to their volume.SnapshotParameters.
// It will not delete a snapshot that is not ready, does not have a remote target, or where local deletion is disabled.
func (d Driver) maybeDeleteLocalSnapshot(ctx context.Context, snap *volume.Snapshot, params *volume.SnapshotParameters) error {
Expand Down
2 changes: 1 addition & 1 deletion pkg/volume/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ type SnapshotCreateDeleter interface {
// List Snapshots should return a sorted list of snapshots.
ListSnaps(ctx context.Context, start, limit int) ([]*Snapshot, error)
// VolFromSnap creates a new volume based on the provided snapshot.
VolFromSnap(ctx context.Context, snap *Snapshot, vol *Info, params *Parameters, topologies *csi.TopologyRequirement) error
VolFromSnap(ctx context.Context, snap *Snapshot, vol *Info, params *Parameters, snapParams *SnapshotParameters, topologies *csi.TopologyRequirement) error
}

// AttacherDettacher handles operations relating to volume accessiblity on nodes.
Expand Down

0 comments on commit 4ed2026

Please sign in to comment.