Skip to content

Commit

Permalink
cinder-csi: Adds support for managing backups (#2473)
Browse files Browse the repository at this point in the history
Signed-off-by: Sebastian-RG <[email protected]>
  • Loading branch information
Sebastian-RG committed Jan 18, 2024
1 parent 35ff405 commit 168136d
Show file tree
Hide file tree
Showing 12 changed files with 756 additions and 101 deletions.
2 changes: 2 additions & 0 deletions docs/cinder-csi-plugin/using-cinder-csi-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ 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 |
| VolumeSnapshotClass `parameters` | `backup-max-duration-seconds-per-gb` | `20` | Defines the amount of time to wait for a backup to complete in seconds per GB of volume size |
| 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 |

Expand Down
327 changes: 273 additions & 54 deletions pkg/csi/cinder/controllerserver.go

Large diffs are not rendered by default.

30 changes: 16 additions & 14 deletions pkg/csi/cinder/controllerserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -462,10 +462,11 @@ func TestListVolumes(t *testing.T) {
// Test CreateSnapshot
func TestCreateSnapshot(t *testing.T) {

osmock.On("CreateSnapshot", FakeSnapshotName, FakeVolID, &map[string]string{cinderCSIClusterIDKey: "cluster"}).Return(&FakeSnapshotRes, nil)
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("WaitSnapshotReady", FakeSnapshotID).Return(FakeSnapshotRes.Status, nil)
osmock.On("ListBackups", map[string]string{"Name": FakeSnapshotName}).Return(FakeBackupListEmpty, nil)
osmock.On("GetSnapshotByID", FakeVolID).Return(&FakeSnapshotRes, nil)
// Init assert
assert := assert.New(t)

Expand Down Expand Up @@ -499,7 +500,7 @@ func TestCreateSnapshotWithExtraMetadata(t *testing.T) {
openstack.SnapshotForceCreate: "true",
}

osmock.On("CreateSnapshot", FakeSnapshotName, FakeVolID, &properties).Return(&FakeSnapshotRes, nil)
osmock.On("CreateSnapshot", FakeSnapshotName, FakeVolID, properties).Return(&FakeSnapshotRes, nil)
osmock.On("ListSnapshots", map[string]string{"Name": FakeSnapshotName}).Return(FakeSnapshotListEmpty, "", nil)
osmock.On("WaitSnapshotReady", FakeSnapshotID).Return(nil)

Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions pkg/csi/cinder/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -94,6 +95,7 @@ var FakeSnapshotRes = snapshots.Snapshot{
Name: "fake-snapshot",
VolumeID: FakeVolID,
Size: 1,
Status: "available",
}

var FakeSnapshotsRes = []snapshots.Snapshot{FakeSnapshotRes}
Expand All @@ -102,6 +104,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"

Expand Down
2 changes: 1 addition & 1 deletion pkg/csi/cinder/nodeserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/csi/cinder/nodeserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 10 additions & 3 deletions pkg/csi/cinder/openstack/openstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -55,11 +56,17 @@ type IOpenStack interface {
GetAttachmentDiskPath(instanceID, volumeID string) (string, error)
GetVolume(volumeID string) (*volumes.Volume, error)
GetVolumesByName(name string) ([]volumes.Volume, error)
CreateSnapshot(name, volID string, tags *map[string]string) (*snapshots.Snapshot, error)
CreateSnapshot(name, volID string, tags map[string]string) (*snapshots.Snapshot, error)
ListSnapshots(filters map[string]string) ([]snapshots.Snapshot, string, error)
DeleteSnapshot(snapID string) error
GetSnapshotByID(snapshotID string) (*snapshots.Snapshot, error)
WaitSnapshotReady(snapshotID string) error
WaitSnapshotReady(snapshotID string) (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)
BackupsAreEnabled() (bool, error)
WaitBackupReady(backupID string, snapshotSize int, backupMaxDurationSecondsPerGB int) (string, error)
GetInstanceByID(instanceID string) (*servers.Server, error)
ExpandVolume(volumeID string, status string, size int) error
GetMaxVolLimit() int64
Expand Down
221 changes: 221 additions & 0 deletions pkg/csi/cinder/openstack/openstack_backups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
Copyright 2023 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 backups provides an implementation of Cinder Backup features
// cinder functions using Gophercloud.
package openstack

import (
"errors"
"fmt"
"strconv"
"time"

"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/backups"
"golang.org/x/net/context"
"k8s.io/cloud-provider-openstack/pkg/metrics"
"k8s.io/klog/v2"
)

const (
backupReadyStatus = "available"
backupErrorStatus = "error"
backupBinary = "cinder-backup"
backupDescription = "Created by OpenStack Cinder CSI driver"
BackupMaxDurationSecondsPerGBDefault = 20
BackupMaxDurationPerGB = "backup-max-duration-seconds-per-gb"
backupBaseDurationSeconds = 30
backupReadyCheckIntervalSeconds = 7
)

// CreateBackup issues a request to create a Backup from the specified Snapshot with the corresponding ID and
// returns the resultant gophercloud Backup Item upon success.
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
}

// ListBackups retrieves a list of active backups from Cinder for the corresponding Tenant. We also
// provide the ability to provide limit and offset to enable the consumer to provide accurate pagination.
// In addition the filters argument provides a mechanism for passing in valid filter strings to the list
// operation. Valid filter keys are: Name, Status, VolumeID, Limit, Marker (TenantID has no effect).
func (os *OpenStack) ListBackups(filters map[string]string) ([]backups.Backup, error) {
var allBackups []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")

allPages, err := backups.List(os.blockstorage, opts).AllPages()
if err != nil {
return nil, err
}
allBackups, err = backups.ExtractBackups(allPages)
if err != nil {
return nil, err
}

if mc.ObserveRequest(err) != nil {
return nil, err
}

return allBackups, nil
}

// DeleteBackup issues a request to delete the Backup with the specified ID from the Cinder backend.
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
}

// GetBackupByID returns backup details by id.
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 backup: %v", err)
return nil, err
}
return backup, nil
}

func (os *OpenStack) BackupsAreEnabled() (bool, error) {
// TODO: Check if the backup service is enabled
return true, nil
}

// WaitBackupReady waits until backup is ready. It waits longer depending on
// the size of the corresponding snapshot.
func (os *OpenStack) WaitBackupReady(backupID string, snapshotSize int, backupMaxDurationSecondsPerGB int) (string, error) {
var err error

duration := time.Duration(backupMaxDurationSecondsPerGB*snapshotSize + backupBaseDurationSeconds)

err = os.waitBackupReadyWithContext(backupID, duration)
if err == context.DeadlineExceeded {
err = fmt.Errorf("timeout, Backup %s is still not Ready: %v", backupID, err)
}

back, _ := os.GetBackupByID(backupID)

if back != nil {
return back.Status, err
} else {
return "Failed to get backup status", err
}
}

// Supporting function for WaitBackupReady().
// Allows for a timeout while waiting for the backup to be ready.
func (os *OpenStack) waitBackupReadyWithContext(backupID string, duration time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), duration*time.Second)
defer cancel()
var done bool
var err error
ticker := time.NewTicker(backupReadyCheckIntervalSeconds * time.Second)
defer ticker.Stop()

for {
select {
case <-ticker.C:
done, err = os.backupIsReady(backupID)
if err != nil {
return err
}

if done {
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}

}

// Supporting function for waitBackupReadyWithContext().
// Returns true when the backup is ready.
func (os *OpenStack) backupIsReady(backupID string) (bool, error) {
backup, err := os.GetBackupByID(backupID)
if err != nil {
return false, err
}

if backup.Status == backupErrorStatus {
return false, errors.New("backup is in error state")
}

return backup.Status == backupReadyStatus, nil
}
Loading

0 comments on commit 168136d

Please sign in to comment.