Skip to content

Commit c9e5c05

Browse files
committed
OKE (CSI) Expand Volume support.
1 parent 92c4c53 commit c9e5c05

File tree

30 files changed

+1026
-152
lines changed

30 files changed

+1026
-152
lines changed

cmd/oci-csi-controller-driver/csioptions/csioptions.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type CSIOptions struct {
4444
MetricsPath string
4545
ExtraCreateMetadata bool
4646
ReconcileSync time.Duration
47+
EnableResizer bool
4748
}
4849

4950
//NewCSIOptions initializes the flag
@@ -71,6 +72,7 @@ func NewCSIOptions() *CSIOptions {
7172
MetricsPath: *flag.String("metrics-path", "/metrics", "The HTTP path where prometheus metrics will be exposed. Default is `/metrics`."),
7273
ExtraCreateMetadata: *flag.Bool("extra-create-metadata", false, "If set, add pv/pvc metadata to plugin create requests as parameters."),
7374
ReconcileSync: *flag.Duration("reconcile-sync", 1*time.Minute, "Resync interval of the VolumeAttachment reconciler."),
75+
EnableResizer: *flag.Bool("csi-bv-expansion-enabled", false, "Enables go routine csi-resizer."),
7476
}
7577
return &csioptions
7678
}

cmd/oci-csi-node-driver/main.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ package main
1616

1717
import (
1818
"flag"
19+
20+
"github.com/spf13/viper"
21+
"go.uber.org/zap/zapcore"
22+
"k8s.io/klog"
23+
1924
"github.com/oracle/oci-cloud-controller-manager/cmd/oci-csi-node-driver/nodedriver"
2025
"github.com/oracle/oci-cloud-controller-manager/cmd/oci-csi-node-driver/nodedriveroptions"
2126
"github.com/oracle/oci-cloud-controller-manager/cmd/oci-csi-node-driver/nodedriverregistrar"
2227
"github.com/oracle/oci-cloud-controller-manager/pkg/csi/driver"
2328
"github.com/oracle/oci-cloud-controller-manager/pkg/util/signals"
24-
"k8s.io/klog"
2529
)
2630

2731
func main() {
@@ -44,6 +48,8 @@ func main() {
4448
flag.Set("logtostderr", "true")
4549
flag.Parse()
4650

51+
viper.Set("log-level", getLevel(nodecsioptions.LogLevel))
52+
4753
blockvolumeNodeOptions := nodedriveroptions.NodeOptions{
4854
Name: "BV",
4955
Endpoint: nodecsioptions.Endpoint,
@@ -72,3 +78,24 @@ func main() {
7278
}
7379
<-stopCh
7480
}
81+
82+
func getLevel(loglevel string) int8 {
83+
switch loglevel {
84+
case "debug":
85+
return int8(zapcore.DebugLevel)
86+
case "info":
87+
return int8(zapcore.InfoLevel)
88+
case "warn":
89+
return int8(zapcore.WarnLevel)
90+
case "error":
91+
return int8(zapcore.ErrorLevel)
92+
case "dpanic":
93+
return int8(zapcore.DPanicLevel)
94+
case "panic":
95+
return int8(zapcore.PanicLevel)
96+
case "fatal":
97+
return int8(zapcore.FatalLevel)
98+
default:
99+
return int8(zapcore.InfoLevel)
100+
}
101+
}

pkg/cloudprovider/providers/oci/instances_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import (
2020
"reflect"
2121
"testing"
2222

23+
"go.uber.org/zap"
24+
v1 "k8s.io/api/core/v1"
2325
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/labels"
2427
"k8s.io/apimachinery/pkg/types"
2528

2629
providercfg "github.com/oracle/oci-cloud-controller-manager/pkg/cloudprovider/providers/oci/config"
@@ -30,9 +33,6 @@ import (
3033
"github.com/oracle/oci-go-sdk/v31/filestorage"
3134
"github.com/oracle/oci-go-sdk/v31/identity"
3235
"github.com/oracle/oci-go-sdk/v31/loadbalancer"
33-
"go.uber.org/zap"
34-
v1 "k8s.io/api/core/v1"
35-
"k8s.io/apimachinery/pkg/labels"
3636
)
3737

3838
var (
@@ -487,6 +487,10 @@ func (MockBlockStorageClient) CreateVolume(ctx context.Context, details core.Cre
487487
return nil, nil
488488
}
489489

490+
func (c MockBlockStorageClient) UpdateVolume(ctx context.Context, volumeId string, details core.UpdateVolumeDetails) (*core.Volume, error) {
491+
return nil, nil
492+
}
493+
490494
func (MockBlockStorageClient) GetVolume(ctx context.Context, id string) (*core.Volume, error) {
491495
return nil, nil
492496
}

pkg/csi-util/utils.go

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"time"
1111

1212
"go.uber.org/zap"
13-
13+
"github.com/container-storage-interface/spec/lib/go/csi"
1414
kubeAPI "k8s.io/api/core/v1"
1515
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1616
"k8s.io/apimachinery/pkg/util/sets"
@@ -24,6 +24,18 @@ import (
2424
)
2525

2626
const (
27+
// minimumVolumeSizeInBytes is used to validate that the user is not trying
28+
// to create a volume that is smaller than what we support
29+
MinimumVolumeSizeInBytes int64 = 50 * client.GiB
30+
31+
// maximumVolumeSizeInBytes is used to validate that the user is not trying
32+
// to create a volume that is larger than what we support
33+
MaximumVolumeSizeInBytes int64 = 32 * client.TiB
34+
35+
// defaultVolumeSizeInBytes is used when the user did not provide a size or
36+
// the size they provided did not satisfy our requirements
37+
defaultVolumeSizeInBytes int64 = MinimumVolumeSizeInBytes
38+
2739
waitForPathDelay = 1 * time.Second
2840

2941
// ociVolumeBackupID is the name of the oci volume backup id annotation.
@@ -254,3 +266,64 @@ func (vl *VolumeLocks) Release(volumeID string) {
254266
defer vl.mux.Unlock()
255267
vl.locks.Delete(volumeID)
256268
}
269+
270+
// extractStorage extracts the storage size in bytes from the given capacity
271+
// range. If the capacity range is not satisfied it returns the default volume
272+
// size. If the capacity range is below or above supported sizes, it returns an
273+
// error.
274+
func ExtractStorage(capRange *csi.CapacityRange) (int64, error) {
275+
if capRange == nil {
276+
return defaultVolumeSizeInBytes, nil
277+
}
278+
279+
requiredBytes := capRange.GetRequiredBytes()
280+
requiredSet := 0 < requiredBytes
281+
limitBytes := capRange.GetLimitBytes()
282+
limitSet := 0 < limitBytes
283+
284+
if !requiredSet && !limitSet {
285+
return defaultVolumeSizeInBytes, nil
286+
}
287+
288+
if requiredSet && limitSet && limitBytes < requiredBytes {
289+
return 0, fmt.Errorf("limit (%v) can not be less than required (%v) size", FormatBytes(limitBytes), FormatBytes(requiredBytes))
290+
}
291+
292+
if requiredSet && !limitSet {
293+
return MaxOfInt(requiredBytes, MinimumVolumeSizeInBytes), nil
294+
}
295+
296+
if limitSet {
297+
return MaxOfInt(limitBytes, MinimumVolumeSizeInBytes), nil
298+
}
299+
300+
if requiredSet && requiredBytes > MaximumVolumeSizeInBytes {
301+
return 0, fmt.Errorf("required (%v) can not exceed maximum supported volume size (%v)", FormatBytes(requiredBytes), FormatBytes(MaximumVolumeSizeInBytes))
302+
}
303+
304+
if !requiredSet && limitSet && limitBytes > MaximumVolumeSizeInBytes {
305+
return 0, fmt.Errorf("limit (%v) can not exceed maximum supported volume size (%v)", FormatBytes(limitBytes), FormatBytes(MaximumVolumeSizeInBytes))
306+
}
307+
308+
if requiredSet && limitSet {
309+
return MaxOfInt(requiredBytes, limitBytes), nil
310+
}
311+
312+
if requiredSet {
313+
return requiredBytes, nil
314+
}
315+
316+
if limitSet {
317+
return limitBytes, nil
318+
}
319+
320+
return defaultVolumeSizeInBytes, nil
321+
}
322+
323+
func RoundUpSize(volumeSizeBytes int64, allocationUnitBytes int64) int64 {
324+
return (volumeSizeBytes + allocationUnitBytes - 1) / allocationUnitBytes
325+
}
326+
327+
func RoundUpMinSize() int64 {
328+
return RoundUpSize(MinimumVolumeSizeInBytes, 1*client.GiB)
329+
}

pkg/csi/driver/bv_node.go

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
kubeAPI "k8s.io/api/core/v1"
1515

1616
"github.com/oracle/oci-cloud-controller-manager/pkg/csi-util"
17+
"github.com/oracle/oci-cloud-controller-manager/pkg/oci/client"
1718
"github.com/oracle/oci-cloud-controller-manager/pkg/util/disk"
1819
)
1920

@@ -416,18 +417,21 @@ func getDevicePathAndAttachmentType(logger *zap.SugaredLogger, path []string) (s
416417

417418
// NodeGetCapabilities returns the supported capabilities of the node server
418419
func (d BlockVolumeNodeDriver) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) {
419-
nscap := &csi.NodeServiceCapability{
420-
Type: &csi.NodeServiceCapability_Rpc{
421-
Rpc: &csi.NodeServiceCapability_RPC{
422-
Type: csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
420+
var nscaps []*csi.NodeServiceCapability
421+
nodeCaps := []csi.NodeServiceCapability_RPC_Type{csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME, csi.NodeServiceCapability_RPC_EXPAND_VOLUME}
422+
for _, nodeCap := range nodeCaps {
423+
c := &csi.NodeServiceCapability{
424+
Type: &csi.NodeServiceCapability_Rpc{
425+
Rpc: &csi.NodeServiceCapability_RPC{
426+
Type: nodeCap,
427+
},
423428
},
424-
},
429+
}
430+
nscaps = append(nscaps, c)
425431
}
426432

427433
return &csi.NodeGetCapabilitiesResponse{
428-
Capabilities: []*csi.NodeServiceCapability{
429-
nscap,
430-
},
434+
Capabilities: nscaps,
431435
}, nil
432436
}
433437

@@ -461,5 +465,89 @@ func (d BlockVolumeNodeDriver) NodeGetVolumeStats(ctx context.Context, req *csi.
461465

462466
//NodeExpandVolume returns the expand of the volume
463467
func (d BlockVolumeNodeDriver) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) {
464-
return nil, status.Error(codes.Unimplemented, "NodeExpandVolume is not supported yet")
468+
volumeID := req.GetVolumeId()
469+
if len(volumeID) == 0 {
470+
return nil, status.Error(codes.InvalidArgument, "Volume ID not provided")
471+
}
472+
volumePath := req.GetVolumePath()
473+
if len(volumePath) == 0 {
474+
return nil, status.Error(codes.InvalidArgument, "volume path must be provided")
475+
}
476+
477+
logger := d.logger.With("volumeId", req.VolumeId, "volumePath", req.VolumePath)
478+
479+
if acquired := d.volumeLocks.TryAcquire(req.VolumeId); !acquired {
480+
logger.Error("Could not acquire lock for NodeUnpublishVolume.")
481+
return nil, status.Errorf(codes.Aborted, volumeOperationAlreadyExistsFmt, req.VolumeId)
482+
}
483+
484+
defer d.volumeLocks.Release(req.VolumeId)
485+
486+
requestedSize, err:= csi_util.ExtractStorage(req.CapacityRange)
487+
requestedSizeGB := csi_util.RoundUpSize(requestedSize, 1*client.GiB)
488+
489+
if err != nil {
490+
logger.With(zap.Error(err)).Error("invalid capacity range")
491+
return nil, status.Errorf(codes.OutOfRange, "invalid capacity range: %v", err)
492+
}
493+
494+
diskPath, err := disk.GetDiskPathFromMountPath(d.logger, volumePath)
495+
if err != nil {
496+
// do a clean exit in case of mount point not found
497+
if err == disk.ErrMountPointNotFound {
498+
logger.With(zap.Error(err)).With("volumePath", volumePath).Warn("unable to fetch mount point")
499+
return &csi.NodeExpandVolumeResponse{}, nil
500+
}
501+
logger.With(zap.Error(err)).With("volumePath", volumePath).Error("unable to get diskPath from mount path")
502+
return nil, status.Error(codes.Internal, err.Error())
503+
}
504+
505+
attachmentType, devicePath, err := getDevicePathAndAttachmentType(d.logger, diskPath)
506+
if err != nil {
507+
logger.With(zap.Error(err)).With("diskPath", diskPath).Error("unable to determine the attachment type")
508+
return nil, status.Error(codes.Internal, err.Error())
509+
}
510+
logger.With("diskPath", diskPath, "attachmentType", attachmentType, "devicePath", devicePath).Infof("Extracted attachment type and device path")
511+
512+
var mountHandler disk.Interface
513+
switch attachmentType {
514+
case attachmentTypeISCSI:
515+
scsiInfo, _ := csi_util.ExtractISCSIInformationFromMountPath(d.logger, diskPath)
516+
if scsiInfo == nil {
517+
logger.Warn("unable to get the ISCSI info")
518+
return &csi.NodeExpandVolumeResponse{}, nil
519+
}
520+
mountHandler = disk.NewFromISCSIDisk(d.logger, scsiInfo)
521+
d.logger.With("ISCSIInfo", scsiInfo, "mountPath", volumePath).Info("Found ISCSIInfo for NodeExpandVolume.")
522+
case attachmentTypeParavirtualized:
523+
mountHandler = disk.NewFromPVDisk(d.logger)
524+
logger.Info("starting to expand paravirtualized Mounting.")
525+
default:
526+
logger.Error("unknown attachment type. supported attachment types are iscsi and paravirtualized")
527+
return nil, status.Error(codes.InvalidArgument, "unknown attachment type. supported attachment types are iscsi and paravirtualized")
528+
}
529+
530+
if err := mountHandler.Rescan(devicePath); err != nil {
531+
return nil, status.Errorf(codes.Internal, "Failed to rescan volume %q (%q): %v", volumeID, devicePath, err)
532+
}
533+
logger.With("devicePath", devicePath).Debug("Rescan completed")
534+
535+
if _, err := mountHandler.Resize(devicePath, volumePath); err != nil {
536+
return nil, status.Errorf(codes.Internal, "Failed to resize volume %q (%q): %v", volumeID, devicePath, err)
537+
}
538+
539+
allocatedSizeBytes, err := mountHandler.GetBlockSizeBytes(devicePath)
540+
if err != nil {
541+
return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to get size of block volume at path %s: %v", devicePath, err))
542+
}
543+
544+
allocatedSizeGB := csi_util.RoundUpSize(allocatedSizeBytes, 1*client.GiB)
545+
546+
if allocatedSizeGB < requestedSizeGB {
547+
return nil, status.Error(codes.Internal, fmt.Sprintf("Expand Volume Failed, requested size in GB %d but resize allocated only %d", requestedSizeGB, allocatedSizeGB))
548+
}
549+
550+
return &csi.NodeExpandVolumeResponse{
551+
CapacityBytes: allocatedSizeBytes,
552+
}, nil
465553
}

0 commit comments

Comments
 (0)