Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support specifying inode size for filesystem format #1661

Merged
merged 2 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ The AWS EBS CSI Driver supports [tagging](tagging.md) through `StorageClass.para
| "encrypted" | true, false | false | Whether the volume should be encrypted or not. Valid values are "true" or "false". |
| "blockExpress" | true, false | false | Enables the creation of [io2 Block Express volumes](https://aws.amazon.com/ebs/provisioned-iops/#Introducing_io2_Block_Express) by increasing the IOPS limit for io2 volumes to 256000. Volumes created with more than 64000 IOPS will fail to mount on instances that do not support io2 Block Express. |
| "kmsKeyId" | | | The full ARN of the key to use when encrypting the volume. If not specified, AWS will use the default KMS key for the region the volume is in. This will be an auto-generated key called `/aws/ebs` if not changed. |
| "blockSize" | | | The block size to use when formatting the underlying filesystem. Only supported on linux nodes and with fstype `ext2`, `ext3`, `ext4`, or `xfs`. |
| "blockSize" | | | The block size to use when formatting the underlying filesystem. Only supported on linux nodes and with fstype `ext2`, `ext3`, `ext4`, or `xfs`. |
| "inodeSize" | | | The inode size to use when formatting the underlying filesystem. Only supported on linux nodes and with fstype `ext2`, `ext3`, `ext4`, or `xfs`. |
| "bytesPerINode" | | | The `bytes-per-inode` to use when formatting the underlying filesystem. Only supported on linux nodes and with fstype `ext2`, `ext3`, `ext4`. |
| "numberOfINodes" | | | The `number-of-inodes` to use when formatting the underlying filesystem. Only supported on linux nodes and with fstype `ext2`, `ext3`, `ext4`. |

**Appendix**
* `gp3` is currently not supported on outposts. Outpost customers need to use a different type for their volumes.
Expand Down
55 changes: 47 additions & 8 deletions pkg/driver/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ const (
// BlockSizeKey configures the block size when formatting a volume
BlockSizeKey = "blocksize"

// INodeSizeKey configures the inode size when formatting a volume
INodeSizeKey = "inodesize"

// BytesPerINodeKey configures the `bytes-per-inode` when formatting a volume
BytesPerINodeKey = "bytesperinode"

// NumberOfINodesKey configures the `number-of-inodes` when formatting a volume
NumberOfINodesKey = "numberofinodes"

// TagKeyPrefix contains the prefix of a volume parameter that designates it as
// a tag to be attached to the resource
TagKeyPrefix = "tagSpecification"
Expand Down Expand Up @@ -161,15 +170,45 @@ const (
FSTypeNtfs = "ntfs"
)

// BlockSizeExcludedFSTypes contains the filesystems that a custom block size is *NOT* supported on
var (
BlockSizeExcludedFSTypes = map[string]struct{}{
FSTypeNtfs: {},
}
)

// constants for node k8s API use
const (
// AgentNotReadyTaintKey contains the key of taints to be removed on driver startup
// AgentNotReadyNodeTaintKey contains the key of taints to be removed on driver startup
AgentNotReadyNodeTaintKey = "ebs.csi.aws.com/agent-not-ready"
)

type fileSystemConfig struct {
NotSupportedParams map[string]struct{}
}

func (fsConfig fileSystemConfig) isParameterSupported(paramName string) bool {
_, notSupported := fsConfig.NotSupportedParams[paramName]
return !notSupported
}

var (
FileSystemConfigs = map[string]fileSystemConfig{
FSTypeExt2: {
NotSupportedParams: map[string]struct{}{},
},
FSTypeExt3: {
NotSupportedParams: map[string]struct{}{},
},
FSTypeExt4: {
NotSupportedParams: map[string]struct{}{},
},
FSTypeXfs: {
NotSupportedParams: map[string]struct{}{
BytesPerINodeKey: {},
NumberOfINodesKey: {},
},
},
FSTypeNtfs: {
NotSupportedParams: map[string]struct{}{
BlockSizeKey: {},
INodeSizeKey: {},
BytesPerINodeKey: {},
NumberOfINodesKey: {},
},
},
}
)
94 changes: 69 additions & 25 deletions pkg/driver/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol
cloud.VolumeNameTagKey: volName,
cloud.AwsEbsDriverTagKey: isManagedByDriver,
}
blockSize string
blockSize string
inodeSize string
bytesPerINode string
numberOfINodes string
)

tProps := new(template.PVProps)
Expand Down Expand Up @@ -188,6 +191,24 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol
return nil, status.Errorf(codes.InvalidArgument, "Could not parse blockSize (%s): %v", value, err)
}
blockSize = value
case INodeSizeKey:
_, err = strconv.Atoi(value)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Could not parse inodeSize (%s): %v", value, err)
}
inodeSize = value
case BytesPerINodeKey:
_, err = strconv.Atoi(value)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Could not parse bytesPerINode (%s): %v", value, err)
}
bytesPerINode = value
case NumberOfINodesKey:
_, err = strconv.Atoi(value)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Could not parse numberOfINodes (%s): %v", value, err)
}
numberOfINodes = value
default:
if strings.HasPrefix(key, TagKeyPrefix) {
scTags = append(scTags, value)
Expand All @@ -197,23 +218,30 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol
}
}

if len(blockSize) > 0 {
for _, volCap := range req.GetVolumeCapabilities() {
switch volCap.GetAccessType().(type) {
case *csi.VolumeCapability_Block:
return nil, status.Error(codes.InvalidArgument, "Cannot use block size with block volume")
}

mountVolume := volCap.GetMount()
if mountVolume == nil {
return nil, status.Error(codes.InvalidArgument, "CreateVolume: mount is nil within volume capability")
}

fsType := mountVolume.GetFsType()
responseCtx := map[string]string{}

if _, ok := BlockSizeExcludedFSTypes[fsType]; ok {
return nil, status.Errorf(codes.InvalidArgument, "Cannot use block size with fstype %s", fsType)
}
if len(blockSize) > 0 {
responseCtx[BlockSizeKey] = blockSize
if err = validateVolumeCapabilities(req.GetVolumeCapabilities(), BlockSizeKey, FileSystemConfigs); err != nil {
return nil, err
}
}
if len(inodeSize) > 0 {
responseCtx[INodeSizeKey] = inodeSize
if err = validateVolumeCapabilities(req.GetVolumeCapabilities(), INodeSizeKey, FileSystemConfigs); err != nil {
return nil, err
}
}
if len(bytesPerINode) > 0 {
responseCtx[BytesPerINodeKey] = bytesPerINode
if err = validateVolumeCapabilities(req.GetVolumeCapabilities(), BytesPerINodeKey, FileSystemConfigs); err != nil {
return nil, err
}
}
if len(numberOfINodes) > 0 {
responseCtx[NumberOfINodesKey] = numberOfINodes
if err = validateVolumeCapabilities(req.GetVolumeCapabilities(), NumberOfINodesKey, FileSystemConfigs); err != nil {
return nil, err
}
}

Expand Down Expand Up @@ -289,7 +317,7 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol
}
return nil, status.Errorf(errCode, "Could not create volume %q: %v", volName, err)
}
return newCreateVolumeResponse(disk, blockSize), nil
return newCreateVolumeResponse(disk, responseCtx), nil
}

func validateCreateVolumeRequest(req *csi.CreateVolumeRequest) error {
Expand Down Expand Up @@ -817,7 +845,7 @@ func getOutpostArn(requirement *csi.TopologyRequirement) string {
return ""
}

func newCreateVolumeResponse(disk *cloud.Disk, blockSize string) *csi.CreateVolumeResponse {
func newCreateVolumeResponse(disk *cloud.Disk, ctx map[string]string) *csi.CreateVolumeResponse {
var src *csi.VolumeContentSource
if disk.SnapshotID != "" {
src = &csi.VolumeContentSource{
Expand All @@ -840,16 +868,11 @@ func newCreateVolumeResponse(disk *cloud.Disk, blockSize string) *csi.CreateVolu
segments[AwsOutpostIDKey] = strings.ReplaceAll(arn.Resource, "outpost/", "")
}

context := map[string]string{}
if len(blockSize) > 0 {
context[BlockSizeKey] = blockSize
}

return &csi.CreateVolumeResponse{
Volume: &csi.Volume{
VolumeId: disk.VolumeID,
CapacityBytes: util.GiBToBytes(disk.CapacityGiB),
VolumeContext: context,
VolumeContext: ctx,
AccessibleTopology: []*csi.Topology{
{
Segments: segments,
Expand Down Expand Up @@ -940,3 +963,24 @@ func BuildOutpostArn(segments map[string]string) string {
segments[AwsOutpostIDKey],
)
}

func validateVolumeCapabilities(volumeCapabilities []*csi.VolumeCapability, paramName string, fsConfigs map[string]fileSystemConfig) error {
for _, volCap := range volumeCapabilities {
switch volCap.GetAccessType().(type) {
case *csi.VolumeCapability_Block:
return status.Error(codes.InvalidArgument, fmt.Sprintf("Cannot use %s with block volume", paramName))
}

mountVolume := volCap.GetMount()
if mountVolume == nil {
return status.Error(codes.InvalidArgument, "CreateVolume: mount is nil within volume capability")
}

fsType := mountVolume.GetFsType()
if supported := fsConfigs[fsType].isParameterSupported(paramName); !supported {
return status.Errorf(codes.InvalidArgument, "Cannot use %s with fstype %s", paramName, fsType)
}
}

return nil
}
106 changes: 63 additions & 43 deletions pkg/driver/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1637,58 +1637,78 @@ func TestCreateVolume(t *testing.T) {
{
name: "success with block size",
testFunc: func(t *testing.T) {
req := &csi.CreateVolumeRequest{
Name: "random-vol-name",
CapacityRange: stdCapRange,
VolumeCapabilities: stdVolCap,
Parameters: map[string]string{
BlockSizeKey: "4096",
},
}
testSuccessWithParameter(t, BlockSizeKey, "4096", stdCapRange, stdVolCap, stdVolSize)
},
},
{
name: "success with inode size",
testFunc: func(t *testing.T) {
testSuccessWithParameter(t, INodeSizeKey, "256", stdCapRange, stdVolCap, stdVolSize)
},
},
{
name: "success with bytes-per-inode",
testFunc: func(t *testing.T) {
testSuccessWithParameter(t, BytesPerINodeKey, "8192", stdCapRange, stdVolCap, stdVolSize)
},
},
{
name: "success with number-of-inodes",
testFunc: func(t *testing.T) {
testSuccessWithParameter(t, NumberOfINodesKey, "13107200", stdCapRange, stdVolCap, stdVolSize)
},
},
}

ctx := context.Background()
for _, tc := range testCases {
t.Run(tc.name, tc.testFunc)
}
}

mockDisk := &cloud.Disk{
VolumeID: req.Name,
AvailabilityZone: expZone,
CapacityGiB: util.BytesToGiB(stdVolSize),
}
func testSuccessWithParameter(t *testing.T, key, value string, capRange *csi.CapacityRange, volCap []*csi.VolumeCapability, volSize int64) {
req := &csi.CreateVolumeRequest{
Name: "random-vol-name",
CapacityRange: capRange,
VolumeCapabilities: volCap,
Parameters: map[string]string{key: value},
}

mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
ctx := context.Background()

mockCloud := cloud.NewMockCloud(mockCtl)
mockCloud.EXPECT().CreateDisk(gomock.Eq(ctx), gomock.Eq(req.Name), gomock.Any()).Return(mockDisk, nil)
mockDisk := &cloud.Disk{
VolumeID: req.Name,
AvailabilityZone: expZone,
CapacityGiB: util.BytesToGiB(volSize),
}

awsDriver := controllerService{
cloud: mockCloud,
inFlight: internal.NewInFlight(),
driverOptions: &DriverOptions{},
}
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()

response, err := awsDriver.CreateVolume(ctx, req)
if err != nil {
srvErr, ok := status.FromError(err)
if !ok {
t.Fatalf("Could not get error status code from error: %v", srvErr)
}
t.Fatalf("Unexpected error: %v", srvErr.Code())
}
mockCloud := cloud.NewMockCloud(mockCtl)
mockCloud.EXPECT().CreateDisk(gomock.Eq(ctx), gomock.Eq(req.Name), gomock.Any()).Return(mockDisk, nil)

context := response.Volume.VolumeContext
if blockSize, ok := context[BlockSizeKey]; ok {
if blockSize != "4096" {
t.Fatalf("Invalid %s in VolumeContext (got %s expected 4096)", BlockSizeKey, blockSize)
}
} else {
t.Fatalf("Missing key %s in VolumeContext", BlockSizeKey)
}
},
},
awsDriver := controllerService{
cloud: mockCloud,
inFlight: internal.NewInFlight(),
driverOptions: &DriverOptions{},
}

for _, tc := range testCases {
t.Run(tc.name, tc.testFunc)
response, err := awsDriver.CreateVolume(ctx, req)
if err != nil {
srvErr, ok := status.FromError(err)
if !ok {
t.Fatalf("Could not get error status code from error: %v", srvErr)
}
t.Fatalf("Unexpected error: %v", srvErr.Code())
}

volCtx := response.Volume.VolumeContext
if sizeValue, ok := volCtx[key]; ok {
if sizeValue != value {
t.Fatalf("Invalid %s in VolumeContext (got %s expected %s)", key, sizeValue, value)
}
} else {
t.Fatalf("Missing key %s in VolumeContext", key)
}
}

Expand Down
Loading