diff --git a/docs/cinder-csi-plugin/using-cinder-csi-plugin.md b/docs/cinder-csi-plugin/using-cinder-csi-plugin.md index 4bcb1eaeed..6102747511 100644 --- a/docs/cinder-csi-plugin/using-cinder-csi-plugin.md +++ b/docs/cinder-csi-plugin/using-cinder-csi-plugin.md @@ -254,6 +254,7 @@ helm install --namespace kube-system --name cinder-csi ./charts/cinder-csi-plugi | StorageClass `parameters` | `availability` | `nova` | String. Volume Availability Zone | | StorageClass `parameters` | `type` | Empty String | String. Name/ID of Volume type. Corresponding volume type should exist in cinder | | VolumeSnapshotClass `parameters` | `force-create` | `false` | Enable to support creating snapshot for a volume in in-use status | +| VolumeSnapshotClass `parameters` | `type` | Empty String | `snapshot` creates a VolumeSnapshot object linked to a Cinder volume snapshot. `backup` creates a VolumeSnapshot object linked to a cinder volume backup. Defaults to `snapshot` if not defined | | Inline Volume `volumeAttributes` | `capacity` | `1Gi` | volume size for creating inline volumes| | Inline Volume `VolumeAttributes` | `type` | Empty String | Name/ID of Volume type. Corresponding volume type should exist in cinder | diff --git a/pkg/csi/cinder/controllerserver.go b/pkg/csi/cinder/controllerserver.go index 6b9674ef50..88035c7fa6 100644 --- a/pkg/csi/cinder/controllerserver.go +++ b/pkg/csi/cinder/controllerserver.go @@ -21,6 +21,7 @@ import ( "strconv" "github.com/container-storage-interface/spec/lib/go/csi" + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/backups" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" "github.com/kubernetes-csi/csi-lib-utils/protosanitizer" @@ -112,15 +113,29 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol content := req.GetVolumeContentSource() var snapshotID string var sourcevolID string + var sourcebackupID string if content != nil && content.GetSnapshot() != nil { snapshotID = content.GetSnapshot().GetSnapshotId() - _, err := cloud.GetSnapshotByID(snapshotID) + snapshotError, err := GetSnapshot(cloud, snapshotID) if err != nil { - if cpoerrors.IsNotFound(err) { - return nil, status.Errorf(codes.NotFound, "VolumeContentSource Snapshot %s not found", snapshotID) - } - return nil, status.Errorf(codes.Internal, "Failed to retrieve the snapshot %s: %v", snapshotID, err) + return nil, err + } + // Look for a Backup with the same ID as the snapshot. + BackupError, err := GetBackup(cloud, snapshotID) + if err != nil { + return nil, err + } + + // If Backup and snapshot not found + if snapshotError != nil && BackupError != nil { + return nil, status.Errorf(codes.NotFound, "VolumeContentSource Snapshot or Backup %s not found", snapshotID) + } + + // Case: Backup found, Snapshot not found. Create Volume from Backup. + if BackupError == nil && snapshotError != nil { + sourcebackupID = snapshotID + snapshotID = "" } } @@ -135,7 +150,11 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol } } - vol, err := cloud.CreateVolume(volName, volSizeGB, volType, volAvailability, snapshotID, sourcevolID, &properties) + vol, err := cloud.CreateVolume(volName, volSizeGB, volType, volAvailability, snapshotID, sourcevolID, sourcebackupID, &properties) + // When creating a volume from a backup, the response does not include the backupID. + if sourcebackupID != "" { + vol.BackupID = &sourcebackupID + } if err != nil { klog.Errorf("Failed to CreateVolume: %v", err) @@ -148,6 +167,38 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol return getCreateVolumeResponse(vol, ignoreVolumeAZ, req.GetAccessibilityRequirements()), nil } +func GetSnapshot(cloud openstack.IOpenStack, snapshotID string) (error, error) { + snap, errsnap := cloud.GetSnapshotByID(snapshotID) + // Case: Snapshot exists but is not yet available + if errsnap == nil && snap.Status != "available" { + return errsnap, status.Errorf(codes.Internal, "Snapshot is not yet available. Status: %s", snap.Status) + } + // Case: General error while getting the Snapshot + if errsnap != nil && !cpoerrors.IsNotFound(errsnap) { + return errsnap, status.Errorf(codes.Internal, "Failed to retrieve the snapshot %s: %v", snapshotID, errsnap) + } + + return errsnap, nil +} + +func GetBackup(cloud openstack.IOpenStack, snapshotID string) (error, error) { + // Backup exists but is not yet available. + back, errback := cloud.GetBackupByID(snapshotID) + if errback == nil && back.Status != "available" { + return errback, status.Errorf(codes.Internal, "Backup is not currently available. Status: %s", back.Status) + } + // Case: Error while getting Backup. + // Do not return error because some openstack deployments do not support backups + if errback != nil { + if cpoerrors.IsNotFound(errback) { + klog.Errorf("VolumeContentSource Backup %s not found", snapshotID) + } else { + klog.Errorf("Failed to retrieve the backup %s: %v", snapshotID, errback) + } + } + return errback, nil +} + func (cs *controllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) { klog.V(4).Infof("DeleteVolume: called with args %+v", protosanitizer.StripSecrets(*req)) @@ -328,6 +379,12 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS name := req.Name volumeID := req.GetSourceVolumeId() + snapshotType, snapshotTypeDeclared := req.Parameters[openstack.SnapshotType] + + // Set snapshot type to 'snapshot' by default + if !snapshotTypeDeclared { + snapshotType = "snapshot" + } if name == "" { return nil, status.Error(codes.InvalidArgument, "Snapshot name must be provided in CreateSnapshot request") @@ -337,6 +394,11 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS return nil, status.Error(codes.InvalidArgument, "VolumeID must be provided in CreateSnapshot request") } + // Verify snapshot type has a valid value + if snapshotType != "snapshot" && snapshotType != "backup" && snapshotType != "" { + return nil, status.Error(codes.InvalidArgument, "Snapshot type must be 'backup', 'snapshot' or not defined") + } + // Verify a snapshot with the provided name doesn't already exist for this tenant var snap *snapshots.Snapshot filters := map[string]string{} @@ -347,6 +409,14 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS return nil, status.Error(codes.Internal, "Failed to get snapshots") } + // Verify a backup with the provided name doesn't already exist for this tenant + var backup *backups.Backup + backups, err := cs.Cloud.ListBackups(filters) + if err != nil { + klog.Errorf("Failed to query for existing Backup during CreateBackups: %v", err) + return nil, status.Error(codes.Internal, "Failed to get backups") + } + if len(snapshots) == 1 { snap = &snapshots[0] @@ -360,6 +430,18 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS klog.Errorf("found multiple existing snapshots with selected name (%s) during create", name) return nil, status.Error(codes.Internal, "Multiple snapshots reported by Cinder with same name") + } else if len(backups) == 1 && snapshotType == "backup" { + backup = &backups[0] + + if backup.VolumeID != volumeID { + return nil, status.Error(codes.AlreadyExists, "Backup with given name already exists, with different source volume ID") + } + klog.V(3).Infof("Found existing backup %s from volume with ID: %s", name, volumeID) + + } else if len(backups) > 1 && snapshotType == "backup" { + klog.Errorf("found multiple existing backups with selected name (%s) during create", name) + return nil, status.Error(codes.Internal, "Multiple backups reported by Cinder with same name") + } else { // Add cluster ID to the snapshot metadata properties := map[string]string{cinderCSIClusterIDKey: cs.Driver.cluster} @@ -394,6 +476,61 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS klog.Errorf("Failed to WaitSnapshotReady: %v", err) return nil, status.Error(codes.Internal, fmt.Sprintf("CreateSnapshot failed with error %v", err)) } + // If snapshotType is 'backup', create a backup from the snapshot and delete the snapshot. + if snapshotType == "backup" { + // Add cluster ID to the snapshot metadata + properties := map[string]string{cinderCSIClusterIDKey: cs.Driver.cluster} + + // see https://github.com/kubernetes-csi/external-snapshotter/pull/375/ + // Also, we don't want to tag every param but we still want to send the + // 'force-create' flag to openstack layer so that we will honor the + // force create functions + for _, mKey := range []string{"csi.storage.k8s.io/volumesnapshot/name", "csi.storage.k8s.io/volumesnapshot/namespace", "csi.storage.k8s.io/volumesnapshotcontent/name", openstack.SnapshotForceCreate, openstack.SnapshotType} { + if v, ok := req.Parameters[mKey]; ok { + properties[mKey] = v + } + } + backup, err = cs.Cloud.CreateBackup(name, volumeID, snap.ID, &properties) + if err != nil { + klog.Errorf("Failed to Create backup: %v", err) + return nil, status.Error(codes.Internal, fmt.Sprintf("CreateBackup failed with error %v", err)) + } + klog.V(4).Infof("Backup created: %+v", backup) + + ctime = timestamppb.New(backup.CreatedAt) + if err := ctime.CheckValid(); err != nil { + klog.Errorf("Error to convert time to timestamp: %v", err) + } + + err = cs.Cloud.WaitBackupReady(backup.ID) + if err != nil { + klog.Errorf("Failed to WaitBackupReady: %v", err) + return nil, status.Error(codes.Internal, fmt.Sprintf("CreateBackup failed with error %v", err)) + } + + err = cs.Cloud.DeleteSnapshot(snap.ID) + if err != nil { + klog.Errorf("Failed to DeleteSnapshot: %v", err) + return nil, status.Error(codes.Internal, fmt.Sprintf("DeleteSnapshot failed with error %v", err)) + } + + // Necessary to get all the backup information, including size. + backup, err = cs.Cloud.GetBackupByID(backup.ID) + if err != nil { + klog.Errorf("Failed to GetBackupByID after backup creation: %v", err) + return nil, status.Error(codes.Internal, fmt.Sprintf("GetBackupByID failed with error %v", err)) + } + + return &csi.CreateSnapshotResponse{ + Snapshot: &csi.Snapshot{ + SnapshotId: backup.ID, + SizeBytes: int64(backup.Size * 1024 * 1024 * 1024), + SourceVolumeId: backup.VolumeID, + CreationTime: ctime, + ReadyToUse: true, + }, + }, nil + } return &csi.CreateSnapshotResponse{ Snapshot: &csi.Snapshot{ @@ -415,8 +552,23 @@ func (cs *controllerServer) DeleteSnapshot(ctx context.Context, req *csi.DeleteS return nil, status.Error(codes.InvalidArgument, "Snapshot ID must be provided in DeleteSnapshot request") } + // If volumeSnapshot object was linked to a cinder backup, delete the backup. + back, err := cs.Cloud.GetBackupByID(id) + if err == nil && back != nil { + err = cs.Cloud.DeleteBackup(id) + if err != nil { + if cpoerrors.IsNotFound(err) { + klog.V(3).Infof("Backup %s is already deleted.", id) + return &csi.DeleteSnapshotResponse{}, nil + } + klog.Errorf("Failed to Delete backup: %v", err) + return nil, status.Error(codes.Internal, fmt.Sprintf("DeleteBackup failed with error %v", err)) + } + return &csi.DeleteSnapshotResponse{}, nil + } + // Delegate the check to openstack itself - err := cs.Cloud.DeleteSnapshot(id) + err = cs.Cloud.DeleteSnapshot(id) if err != nil { if cpoerrors.IsNotFound(err) { klog.V(3).Infof("Snapshot %s is already deleted.", id) @@ -677,6 +829,16 @@ func getCreateVolumeResponse(vol *volumes.Volume, ignoreVolumeAZ bool, accessibl } } + if vol.BackupID != nil && *vol.BackupID != "" { + volsrc = &csi.VolumeContentSource{ + Type: &csi.VolumeContentSource_Snapshot{ + Snapshot: &csi.VolumeContentSource_SnapshotSource{ + SnapshotId: *vol.BackupID, + }, + }, + } + } + var accessibleTopology []*csi.Topology // If ignore-volume-az is true , dont set the accessible topology to volume az, // use from preferred topologies instead. diff --git a/pkg/csi/cinder/controllerserver_test.go b/pkg/csi/cinder/controllerserver_test.go index eb32140a4c..026f5f106c 100644 --- a/pkg/csi/cinder/controllerserver_test.go +++ b/pkg/csi/cinder/controllerserver_test.go @@ -45,8 +45,8 @@ func TestCreateVolume(t *testing.T) { // mock OpenStack properties := map[string]string{"cinder.csi.openstack.org/cluster": FakeCluster} - // CreateVolume(name string, size int, vtype, availability string, snapshotID string, tags *map[string]string) (string, string, int, error) - osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), FakeVolType, FakeAvailability, "", "", &properties).Return(&FakeVol, nil) + // CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourcevolID string, sourcebackupID string, tags *map[string]string) (string, string, int, error) + osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), FakeVolType, FakeAvailability, "", "", "", &properties).Return(&FakeVol, nil) osmock.On("GetVolumesByName", FakeVolName).Return(FakeVolListEmpty, nil) // Init assert @@ -92,9 +92,9 @@ func TestCreateVolumeWithParam(t *testing.T) { // mock OpenStack properties := map[string]string{"cinder.csi.openstack.org/cluster": FakeCluster} - // CreateVolume(name string, size int, vtype, availability string, snapshotID string, tags *map[string]string) (string, string, int, error) + // CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourcevolID string, sourcebackupID string, tags *map[string]string) (string, string, int, error) // Vol type and availability comes from CreateVolumeRequest.Parameters - osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), "dummyVolType", "cinder", "", "", &properties).Return(&FakeVol, nil) + osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), "dummyVolType", "cinder", "", "", "", &properties).Return(&FakeVol, nil) osmock.On("GetVolumesByName", FakeVolName).Return(FakeVolListEmpty, nil) // Init assert @@ -149,8 +149,8 @@ func TestCreateVolumeWithExtraMetadata(t *testing.T) { "csi.storage.k8s.io/pvc/name": FakePVCName, "csi.storage.k8s.io/pvc/namespace": FakePVCNamespace, } - // CreateVolume(name string, size int, vtype, availability string, snapshotID string, tags *map[string]string) (string, string, int, error) - osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), FakeVolType, FakeAvailability, "", "", &properties).Return(&FakeVol, nil) + // CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourcevolID string, sourcebackupID string, tags *map[string]string) (string, string, int, error) + osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), FakeVolType, FakeAvailability, "", "", "", &properties).Return(&FakeVol, nil) osmock.On("GetVolumesByName", FakeVolName).Return(FakeVolListEmpty, nil) @@ -190,8 +190,8 @@ func TestCreateVolumeWithExtraMetadata(t *testing.T) { func TestCreateVolumeFromSnapshot(t *testing.T) { properties := map[string]string{"cinder.csi.openstack.org/cluster": FakeCluster} - // CreateVolume(name string, size int, vtype, availability string, snapshotID string, tags *map[string]string) (string, string, int, error) - osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), FakeVolType, "", FakeSnapshotID, "", &properties).Return(&FakeVolFromSnapshot, nil) + // CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourcevolID string, sourcebackupID string, tags *map[string]string) (string, string, int, error) + osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), FakeVolType, "", FakeSnapshotID, "", "", &properties).Return(&FakeVolFromSnapshot, nil) osmock.On("GetVolumesByName", FakeVolName).Return(FakeVolListEmpty, nil) // Init assert @@ -238,8 +238,8 @@ func TestCreateVolumeFromSnapshot(t *testing.T) { func TestCreateVolumeFromSourceVolume(t *testing.T) { properties := map[string]string{"cinder.csi.openstack.org/cluster": FakeCluster} - // CreateVolume(name string, size int, vtype, availability string, snapshotID string, tags *map[string]string) (string, string, int, error) - osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), FakeVolType, "", "", FakeVolID, &properties).Return(&FakeVolFromSourceVolume, nil) + // CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourcevolID string, sourcebackupID string, tags *map[string]string) (string, string, int, error) + osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), FakeVolType, "", "", FakeVolID, "", &properties).Return(&FakeVolFromSourceVolume, nil) osmock.On("GetVolumesByName", FakeVolName).Return(FakeVolListEmpty, nil) // Init assert @@ -465,6 +465,7 @@ func TestCreateSnapshot(t *testing.T) { osmock.On("CreateSnapshot", FakeSnapshotName, FakeVolID, &map[string]string{cinderCSIClusterIDKey: "cluster"}).Return(&FakeSnapshotRes, nil) osmock.On("ListSnapshots", map[string]string{"Name": FakeSnapshotName}).Return(FakeSnapshotListEmpty, "", nil) osmock.On("WaitSnapshotReady", FakeSnapshotID).Return(nil) + osmock.On("ListBackups", map[string]string{"Name": FakeSnapshotName}).Return(FakeBackupListEmpty, nil) // Init assert assert := assert.New(t) @@ -535,6 +536,7 @@ func TestDeleteSnapshot(t *testing.T) { // DeleteSnapshot(volumeID string) error osmock.On("DeleteSnapshot", FakeSnapshotID).Return(nil) + osmock.On("DeleteBackup", FakeSnapshotID).Return(nil) // Init assert assert := assert.New(t) diff --git a/pkg/csi/cinder/fake.go b/pkg/csi/cinder/fake.go index dfed9220bc..586778f017 100644 --- a/pkg/csi/cinder/fake.go +++ b/pkg/csi/cinder/fake.go @@ -17,6 +17,7 @@ limitations under the License. package cinder import ( + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/backups" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" "golang.org/x/net/context" @@ -102,6 +103,7 @@ var FakeVolListMultiple = []volumes.Volume{FakeVol1, FakeVol3} var FakeVolList = []volumes.Volume{FakeVol1} var FakeVolListEmpty = []volumes.Volume{} var FakeSnapshotListEmpty = []snapshots.Snapshot{} +var FakeBackupListEmpty = []backups.Backup{} var FakeInstanceID = "321a8b81-3660-43e5-bab8-6470b65ee4e8" diff --git a/pkg/csi/cinder/nodeserver.go b/pkg/csi/cinder/nodeserver.go index 2655ed6dcb..c665c8985a 100644 --- a/pkg/csi/cinder/nodeserver.go +++ b/pkg/csi/cinder/nodeserver.go @@ -150,7 +150,7 @@ func nodePublishEphemeral(req *csi.NodePublishVolumeRequest, ns *nodeServer) (*c volumeType = "" } - evol, err := ns.Cloud.CreateVolume(volName, size, volumeType, volAvailability, "", "", &properties) + evol, err := ns.Cloud.CreateVolume(volName, size, volumeType, volAvailability, "", "", "", &properties) if err != nil { klog.V(3).Infof("Failed to Create Ephemeral Volume: %v", err) diff --git a/pkg/csi/cinder/nodeserver_test.go b/pkg/csi/cinder/nodeserver_test.go index d6762dc788..334eacb9fb 100644 --- a/pkg/csi/cinder/nodeserver_test.go +++ b/pkg/csi/cinder/nodeserver_test.go @@ -129,7 +129,7 @@ func TestNodePublishVolumeEphermeral(t *testing.T) { fvolName := fmt.Sprintf("ephemeral-%s", FakeVolID) tState := []string{"available"} - omock.On("CreateVolume", fvolName, 2, "test", "nova", "", "", &properties).Return(&FakeVol, nil) + omock.On("CreateVolume", fvolName, 2, "test", "nova", "", "", "", &properties).Return(&FakeVol, nil) omock.On("AttachVolume", FakeNodeID, FakeVolID).Return(FakeVolID, nil) omock.On("WaitDiskAttached", FakeNodeID, FakeVolID).Return(nil) diff --git a/pkg/csi/cinder/openstack/openstack.go b/pkg/csi/cinder/openstack/openstack.go index e3a76a167f..72cb8096ca 100644 --- a/pkg/csi/cinder/openstack/openstack.go +++ b/pkg/csi/cinder/openstack/openstack.go @@ -23,6 +23,7 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/backups" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" @@ -44,7 +45,7 @@ func AddExtraFlags(fs *pflag.FlagSet) { } type IOpenStack interface { - CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourcevolID string, tags *map[string]string) (*volumes.Volume, error) + CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourcevolID string, sourcebackupID string, tags *map[string]string) (*volumes.Volume, error) DeleteVolume(volumeID string) error AttachVolume(instanceID, volumeID string) (string, error) ListVolumes(limit int, startingToken string) ([]volumes.Volume, string, error) @@ -60,6 +61,11 @@ type IOpenStack interface { DeleteSnapshot(snapID string) error GetSnapshotByID(snapshotID string) (*snapshots.Snapshot, error) WaitSnapshotReady(snapshotID string) error + CreateBackup(name, volID string, snapshotID string, tags *map[string]string) (*backups.Backup, error) + ListBackups(filters map[string]string) ([]backups.Backup, error) + DeleteBackup(backupID string) error + GetBackupByID(backupID string) (*backups.Backup, error) + WaitBackupReady(backupID string) error GetInstanceByID(instanceID string) (*servers.Server, error) ExpandVolume(volumeID string, status string, size int) error GetMaxVolLimit() int64 diff --git a/pkg/csi/cinder/openstack/openstack_backups.go b/pkg/csi/cinder/openstack/openstack_backups.go new file mode 100644 index 0000000000..12ff42e044 --- /dev/null +++ b/pkg/csi/cinder/openstack/openstack_backups.go @@ -0,0 +1,171 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package openstack snapshots provides an implementation of Cinder Snapshot features +// cinder functions using Gophercloud. +package openstack + +import ( + "fmt" + "strconv" + "time" + + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/backups" + "github.com/gophercloud/gophercloud/pagination" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/cloud-provider-openstack/pkg/metrics" + "k8s.io/klog/v2" +) + +const ( + backupReadyStatus = "available" + backupDescription = "Created by OpenStack Cinder CSI driver" + backupReadyDuration = 5 * time.Second + backupReadyFactor = 1.2 + backupReadySteps = 15 +) + +func (os *OpenStack) CreateBackup(name, volID string, snapshotID string, tags *map[string]string) (*backups.Backup, error) { + blockstorageServiceClient, err := openstack.NewBlockStorageV3(os.blockstorage.ProviderClient, os.epOpts) + if err != nil { + return &backups.Backup{}, err + } + + force := false + // if no flag given, then force will be false by default + // if flag it given , check it + if item, ok := (*tags)[SnapshotForceCreate]; ok { + var err error + force, err = strconv.ParseBool(item) + if err != nil { + klog.V(5).Infof("Make force create flag to false due to: %v", err) + } + delete(*tags, SnapshotForceCreate) + } + + opts := &backups.CreateOpts{ + VolumeID: volID, + SnapshotID: snapshotID, + Name: name, + Force: force, + Description: backupDescription, + } + + if tags != nil { + // Set openstack microversion to 3.43 to send metadata along with the backup + blockstorageServiceClient.Microversion = "3.43" + opts.Metadata = *tags + } + + // TODO: Do some check before really call openstack API on the input + mc := metrics.NewMetricContext("backup", "create") + backup, err := backups.Create(blockstorageServiceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return &backups.Backup{}, err + } + // There's little value in rewrapping these gophercloud types into yet another abstraction/type, instead just + // return the gophercloud item + return backup, nil +} + +func (os *OpenStack) ListBackups(filters map[string]string) ([]backups.Backup, error) { + var backs []backups.Backup + + // Build the Opts + opts := backups.ListOpts{} + for key, val := range filters { + switch key { + case "Status": + opts.Status = val + case "Name": + opts.Name = val + case "VolumeID": + opts.VolumeID = val + case "Marker": + opts.Marker = val + case "Limit": + opts.Limit, _ = strconv.Atoi(val) + default: + klog.V(3).Infof("Not a valid filter key %s", key) + } + } + mc := metrics.NewMetricContext("backup", "list") + err := backups.List(os.blockstorage, opts).EachPage(func(page pagination.Page) (bool, error) { + backsPage, err := backups.ExtractBackups(page) + if err != nil { + return false, err + } + backs = append(backs, backsPage...) + return true, nil + }) + if mc.ObserveRequest(err) != nil { + return nil, err + } + + return backs, nil +} + +func (os *OpenStack) DeleteBackup(backupID string) error { + mc := metrics.NewMetricContext("backup", "delete") + err := backups.Delete(os.blockstorage, backupID).ExtractErr() + if mc.ObserveRequest(err) != nil { + klog.Errorf("Failed to delete backup: %v", err) + } + return err +} + +func (os *OpenStack) GetBackupByID(backupID string) (*backups.Backup, error) { + mc := metrics.NewMetricContext("backup", "get") + backup, err := backups.Get(os.blockstorage, backupID).Extract() + if mc.ObserveRequest(err) != nil { + klog.Errorf("Failed to get snapshot: %v", err) + return nil, err + } + return backup, nil +} + +func (os *OpenStack) WaitBackupReady(backupID string) error { + backoff := wait.Backoff{ + Duration: backupReadyDuration, + Factor: backupReadyFactor, + Steps: backupReadySteps, + } + + // Waits for a maximum of 295 seconds + err := wait.ExponentialBackoff(backoff, func() (bool, error) { + ready, err := os.backupIsReady(backupID) + if err != nil { + return false, err + } + return ready, nil + }) + + if wait.Interrupted(err) { + err = fmt.Errorf("Timeout, Backup %s is still not Ready %v", backupID, err) + } + + return err +} + +func (os *OpenStack) backupIsReady(backupID string) (bool, error) { + backup, err := os.GetBackupByID(backupID) + if err != nil { + return false, err + } + + return backup.Status == backupReadyStatus, nil +} diff --git a/pkg/csi/cinder/openstack/openstack_mock.go b/pkg/csi/cinder/openstack/openstack_mock.go index 5c52cf0fbf..123e568d2e 100644 --- a/pkg/csi/cinder/openstack/openstack_mock.go +++ b/pkg/csi/cinder/openstack/openstack_mock.go @@ -17,6 +17,7 @@ limitations under the License. package openstack import ( + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/backups" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" @@ -41,6 +42,18 @@ var fakeSnapshot = snapshots.Snapshot{ Metadata: make(map[string]string), } +var fakemap = make(map[string]string) + +var fakeBackup = backups.Backup{ + ID: "eb5e4e9a-a4e5-4728-a748-04f9e2868573", + Name: "fake-snapshot", + Status: "available", + Size: 1, + VolumeID: "CSIVolumeID", + SnapshotID: "261a8b81-3660-43e5-bab8-6470b65ee4e8", + Metadata: &fakemap, +} + // revive:disable:exported // OpenStackMock is an autogenerated mock type for the IOpenStack type // ORIGINALLY GENERATED BY mockery with hand edits @@ -72,19 +85,19 @@ func (_m *OpenStackMock) AttachVolume(instanceID string, volumeID string) (strin } // CreateVolume provides a mock function with given fields: name, size, vtype, availability, tags -func (_m *OpenStackMock) CreateVolume(name string, size int, vtype string, availability string, snapshotID string, sourceVolID string, tags *map[string]string) (*volumes.Volume, error) { - ret := _m.Called(name, size, vtype, availability, snapshotID, sourceVolID, tags) +func (_m *OpenStackMock) CreateVolume(name string, size int, vtype string, availability string, snapshotID string, sourceVolID string, sourcebackupID string, tags *map[string]string) (*volumes.Volume, error) { + ret := _m.Called(name, size, vtype, availability, snapshotID, sourceVolID, sourcebackupID, tags) var r0 *volumes.Volume - if rf, ok := ret.Get(0).(func(string, int, string, string, string, string, *map[string]string) *volumes.Volume); ok { - r0 = rf(name, size, vtype, availability, snapshotID, sourceVolID, tags) + if rf, ok := ret.Get(0).(func(string, int, string, string, string, string, string, *map[string]string) *volumes.Volume); ok { + r0 = rf(name, size, vtype, availability, snapshotID, sourceVolID, sourcebackupID, tags) } else { r0 = ret.Get(0).(*volumes.Volume) } var r1 error - if rf, ok := ret.Get(1).(func(string, int, string, string, string, string, *map[string]string) error); ok { - r1 = rf(name, size, vtype, availability, snapshotID, sourceVolID, tags) + if rf, ok := ret.Get(1).(func(string, int, string, string, string, string, string, *map[string]string) error); ok { + r1 = rf(name, size, vtype, availability, snapshotID, sourceVolID, sourcebackupID, tags) } else { r1 = ret.Error(1) } @@ -281,6 +294,62 @@ func (_m *OpenStackMock) DeleteSnapshot(snapID string) error { return r0 } +func (_m *OpenStackMock) ListBackups(filters map[string]string) ([]backups.Backup, error) { + ret := _m.Called(filters) + + var r0 []backups.Backup + if rf, ok := ret.Get(0).(func(map[string]string) []backups.Backup); ok { + r0 = rf(filters) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]backups.Backup) + } + } + var r1 error + if rf, ok := ret.Get(1).(func(map[string]string) error); ok { + r1 = rf(filters) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +func (_m *OpenStackMock) CreateBackup(name, volID string, snapshotID string, tags *map[string]string) (*backups.Backup, error) { + ret := _m.Called(name, volID, snapshotID, tags) + + var r0 *backups.Backup + if rf, ok := ret.Get(0).(func(string, string, string, *map[string]string) *backups.Backup); ok { + r0 = rf(name, volID, snapshotID, tags) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*backups.Backup) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, string, *map[string]string) error); ok { + r1 = rf(name, volID, snapshotID, tags) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +func (_m *OpenStackMock) DeleteBackup(backupID string) error { + ret := _m.Called(backupID) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(backupID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // ListVolumes provides a mock function without param func (_m *OpenStackMock) ListVolumes(limit int, marker string) ([]volumes.Volume, string, error) { ret := _m.Called(limit, marker) @@ -355,6 +424,24 @@ func (_m *OpenStackMock) WaitSnapshotReady(snapshotID string) error { return r0 } +func (_m *OpenStackMock) GetBackupByID(backupID string) (*backups.Backup, error) { + + return &fakeBackup, nil +} + +func (_m *OpenStackMock) WaitBackupReady(backupID string) error { + ret := _m.Called(backupID) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(backupID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + func (_m *OpenStackMock) GetMaxVolLimit() int64 { return 256 } diff --git a/pkg/csi/cinder/openstack/openstack_snapshots.go b/pkg/csi/cinder/openstack/openstack_snapshots.go index 00dc9b5c65..c84b1daf2e 100644 --- a/pkg/csi/cinder/openstack/openstack_snapshots.go +++ b/pkg/csi/cinder/openstack/openstack_snapshots.go @@ -39,6 +39,7 @@ const ( snapshotDescription = "Created by OpenStack Cinder CSI driver" SnapshotForceCreate = "force-create" + SnapshotType = "type" ) // CreateSnapshot issues a request to take a Snapshot of the specified Volume with the corresponding ID and diff --git a/pkg/csi/cinder/openstack/openstack_volumes.go b/pkg/csi/cinder/openstack/openstack_volumes.go index 083908e6b4..487370f6fb 100644 --- a/pkg/csi/cinder/openstack/openstack_volumes.go +++ b/pkg/csi/cinder/openstack/openstack_volumes.go @@ -51,7 +51,7 @@ const ( var volumeErrorStates = [...]string{"error", "error_extending", "error_deleting"} // CreateVolume creates a volume of given size -func (os *OpenStack) CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourcevolID string, tags *map[string]string) (*volumes.Volume, error) { +func (os *OpenStack) CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourcevolID string, sourcebackupID string, tags *map[string]string) (*volumes.Volume, error) { opts := &volumes.CreateOpts{ Name: name, @@ -61,13 +61,25 @@ func (os *OpenStack) CreateVolume(name string, size int, vtype, availability str Description: volumeDescription, SnapshotID: snapshotID, SourceVolID: sourcevolID, + BackupID: sourcebackupID, } if tags != nil { opts.Metadata = *tags } + blockstorageClient, err := openstack.NewBlockStorageV3(os.blockstorage.ProviderClient, os.epOpts) + if err != nil { + return nil, err + } + + // creating volumes from backups is available since 3.47 microversion + // https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id47 + if !os.bsOpts.IgnoreVolumeMicroversion && sourcebackupID != "" { + blockstorageClient.Microversion = "3.47" + } + mc := metrics.NewMetricContext("volume", "create") - vol, err := volumes.Create(os.blockstorage, opts).Extract() + vol, err := volumes.Create(blockstorageClient, opts).Extract() if mc.ObserveRequest(err) != nil { return nil, err } diff --git a/tests/sanity/cinder/fakecloud.go b/tests/sanity/cinder/fakecloud.go index e167ddca31..5cc70c19ad 100644 --- a/tests/sanity/cinder/fakecloud.go +++ b/tests/sanity/cinder/fakecloud.go @@ -7,6 +7,7 @@ import ( "time" "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/backups" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" @@ -19,6 +20,7 @@ type cloud struct { volumes map[string]*volumes.Volume snapshots map[string]*snapshots.Snapshot instances map[string]*servers.Server + backups map[string]*backups.Backup } func getfakecloud() *cloud { @@ -32,7 +34,7 @@ func getfakecloud() *cloud { var _ openstack.IOpenStack = &cloud{} // Fake Cloud -func (cloud *cloud) CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourceVolID string, tags *map[string]string) (*volumes.Volume, error) { +func (cloud *cloud) CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourceVolID string, sourcebackupID string, tags *map[string]string) (*volumes.Volume, error) { vol := &volumes.Volume{ ID: randString(10), @@ -43,6 +45,7 @@ func (cloud *cloud) CreateVolume(name string, size int, vtype, availability stri AvailabilityZone: availability, SnapshotID: snapshotID, SourceVolID: sourceVolID, + BackupID: &sourcebackupID, } cloud.volumes[vol.ID] = vol @@ -154,7 +157,7 @@ func (cloud *cloud) CreateSnapshot(name, volID string, tags *map[string]string) snap := &snapshots.Snapshot{ ID: randString(10), Name: name, - Status: "Available", + Status: "available", VolumeID: volID, CreatedAt: time.Now(), } @@ -224,6 +227,76 @@ func (cloud *cloud) WaitSnapshotReady(snapshotID string) error { return nil } +func (cloud *cloud) CreateBackup(name, volID string, snapshotID string, tags *map[string]string) (*backups.Backup, error) { + + backup := &backups.Backup{ + ID: randString(10), + Name: name, + Status: "Available", + VolumeID: volID, + SnapshotID: snapshotID, + CreatedAt: time.Now(), + } + + cloud.backups[backup.ID] = backup + return backup, nil +} + +func (cloud *cloud) ListBackups(filters map[string]string) ([]backups.Backup, error) { + var backuplist []backups.Backup + startingToken := filters["Marker"] + limitfilter := filters["Limit"] + limit, _ := strconv.Atoi(limitfilter) + name := filters["Name"] + volumeID := filters["VolumeID"] + + for _, value := range cloud.backups { + if volumeID != "" { + if value.VolumeID == volumeID { + backuplist = append(backuplist, *value) + break + } + } else if name != "" { + if value.Name == name { + backuplist = append(backuplist, *value) + break + } + } else { + backuplist = append(backuplist, *value) + } + } + + if startingToken != "" { + t, _ := strconv.Atoi(startingToken) + backuplist = backuplist[t:] + } + if limit != 0 { + backuplist = backuplist[:limit] + } + + return backuplist, nil +} + +func (cloud *cloud) DeleteBackup(backupID string) error { + delete(cloud.backups, backupID) + + return nil +} + +func (cloud *cloud) GetBackupByID(backupID string) (*backups.Backup, error) { + backup, ok := cloud.backups[backupID] + + if !ok { + return nil, notFoundError() + } + + return backup, nil +} + +func (cloud *cloud) WaitBackupReady(backupID string) error { + return nil +} + func randString(n int) string { const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" b := make([]byte, n)