diff --git a/frontend/csi/controller_helpers/kubernetes/config.go b/frontend/csi/controller_helpers/kubernetes/config.go index 401cfd68c..4f06a78a4 100644 --- a/frontend/csi/controller_helpers/kubernetes/config.go +++ b/frontend/csi/controller_helpers/kubernetes/config.go @@ -87,6 +87,9 @@ const ( AnnTieringPolicy = prefix + "/tieringPolicy" AnnTieringMinimumCoolingDays = prefix + "/tieringMinimumCoolingDays" + // AnnUnixGroupID sets the UNIX group ownership (GID) of an ONTAP NAS volume at provisioning time. + AnnUnixGroupID = prefix + "/unixGroupID" + // Pod remediation policy annotation and values AnnPodRemediationPolicyAnnotation = prefix + "/podRemediationPolicy" PodRemediationPolicyDelete = "delete" diff --git a/frontend/csi/controller_helpers/kubernetes/helper.go b/frontend/csi/controller_helpers/kubernetes/helper.go index c73d8e951..104759eba 100644 --- a/frontend/csi/controller_helpers/kubernetes/helper.go +++ b/frontend/csi/controller_helpers/kubernetes/helper.go @@ -927,6 +927,7 @@ func getVolumeConfig( UnixPermissions: getAnnotation(annotations, AnnUnixPermissions), StorageClass: storageClass.Name, BlockSize: getAnnotation(annotations, AnnBlockSize), + UnixGroupID: getAnnotation(annotations, AnnUnixGroupID), FileSystem: getAnnotation(annotations, AnnFileSystem), LUKSEncryption: luksEncryption, SplitOnClone: getAnnotation(annotations, AnnSplitOnClone), diff --git a/mocks/mock_storage_drivers/mock_ontap/mock_ontap_rest_interface.go b/mocks/mock_storage_drivers/mock_ontap/mock_ontap_rest_interface.go index add432046..dded7dacb 100644 --- a/mocks/mock_storage_drivers/mock_ontap/mock_ontap_rest_interface.go +++ b/mocks/mock_storage_drivers/mock_ontap/mock_ontap_rest_interface.go @@ -348,31 +348,31 @@ func (mr *MockRestClientInterfaceMockRecorder) FcpNodeGetName(ctx, fields any) * } // FlexGroupCreate mocks base method. -func (m *MockRestClientInterface) FlexGroupCreate(ctx context.Context, name string, size int, aggrs []string, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int) error { +func (m *MockRestClientInterface) FlexGroupCreate(ctx context.Context, name string, size int, aggrs []string, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int, unixGroupID int64) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FlexGroupCreate", ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve) + ret := m.ctrl.Call(m, "FlexGroupCreate", ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, unixGroupID) ret0, _ := ret[0].(error) return ret0 } // FlexGroupCreate indicates an expected call of FlexGroupCreate. -func (mr *MockRestClientInterfaceMockRecorder) FlexGroupCreate(ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve any) *gomock.Call { +func (mr *MockRestClientInterfaceMockRecorder) FlexGroupCreate(ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, unixGroupID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlexGroupCreate", reflect.TypeOf((*MockRestClientInterface)(nil).FlexGroupCreate), ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlexGroupCreate", reflect.TypeOf((*MockRestClientInterface)(nil).FlexGroupCreate), ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, unixGroupID) } // FlexGroupCreateBalanced mocks base method. -func (m *MockRestClientInterface) FlexGroupCreateBalanced(ctx context.Context, name string, size int, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int) error { +func (m *MockRestClientInterface) FlexGroupCreateBalanced(ctx context.Context, name string, size int, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int, unixGroupID int64) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FlexGroupCreateBalanced", ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve) + ret := m.ctrl.Call(m, "FlexGroupCreateBalanced", ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, unixGroupID) ret0, _ := ret[0].(error) return ret0 } // FlexGroupCreateBalanced indicates an expected call of FlexGroupCreateBalanced. -func (mr *MockRestClientInterfaceMockRecorder) FlexGroupCreateBalanced(ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve any) *gomock.Call { +func (mr *MockRestClientInterfaceMockRecorder) FlexGroupCreateBalanced(ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, unixGroupID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlexGroupCreateBalanced", reflect.TypeOf((*MockRestClientInterface)(nil).FlexGroupCreateBalanced), ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlexGroupCreateBalanced", reflect.TypeOf((*MockRestClientInterface)(nil).FlexGroupCreateBalanced), ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, unixGroupID) } // FlexGroupDestroy mocks base method. @@ -2697,33 +2697,33 @@ func (mr *MockRestClientInterfaceMockRecorder) VolumeCloneSplitStart(ctx, volume } // VolumeCreate mocks base method. -func (m *MockRestClientInterface) VolumeCreate(ctx context.Context, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int, dpVolume bool) (string, error) { +func (m *MockRestClientInterface) VolumeCreate(ctx context.Context, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int, dpVolume bool, unixGroupID int64) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VolumeCreate", ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume) + ret := m.ctrl.Call(m, "VolumeCreate", ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume, unixGroupID) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // VolumeCreate indicates an expected call of VolumeCreate. -func (mr *MockRestClientInterfaceMockRecorder) VolumeCreate(ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume any) *gomock.Call { +func (mr *MockRestClientInterfaceMockRecorder) VolumeCreate(ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume, unixGroupID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeCreate", reflect.TypeOf((*MockRestClientInterface)(nil).VolumeCreate), ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeCreate", reflect.TypeOf((*MockRestClientInterface)(nil).VolumeCreate), ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume, unixGroupID) } // VolumeCreateBalanced mocks base method. -func (m *MockRestClientInterface) VolumeCreateBalanced(ctx context.Context, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int, dpVolume bool) (string, error) { +func (m *MockRestClientInterface) VolumeCreateBalanced(ctx context.Context, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int, dpVolume bool, unixGroupID int64) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VolumeCreateBalanced", ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume) + ret := m.ctrl.Call(m, "VolumeCreateBalanced", ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume, unixGroupID) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // VolumeCreateBalanced indicates an expected call of VolumeCreateBalanced. -func (mr *MockRestClientInterfaceMockRecorder) VolumeCreateBalanced(ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume any) *gomock.Call { +func (mr *MockRestClientInterfaceMockRecorder) VolumeCreateBalanced(ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume, unixGroupID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeCreateBalanced", reflect.TypeOf((*MockRestClientInterface)(nil).VolumeCreateBalanced), ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeCreateBalanced", reflect.TypeOf((*MockRestClientInterface)(nil).VolumeCreateBalanced), ctx, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume, unixGroupID) } // VolumeDestroy mocks base method. diff --git a/mocks/mock_storage_drivers/mock_ontap/mock_ontap_zapi_interface.go b/mocks/mock_storage_drivers/mock_ontap/mock_ontap_zapi_interface.go index c9efc8c06..f4fe70448 100644 --- a/mocks/mock_storage_drivers/mock_ontap/mock_ontap_zapi_interface.go +++ b/mocks/mock_storage_drivers/mock_ontap/mock_ontap_zapi_interface.go @@ -253,18 +253,18 @@ func (mr *MockZapiClientInterfaceMockRecorder) FcpNodeGetNameRequest() *gomock.C } // FlexGroupCreate mocks base method. -func (m *MockZapiClientInterface) FlexGroupCreate(ctx context.Context, name string, size int, aggrs []azgo.AggrNameType, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int) (*azgo.VolumeCreateAsyncResponse, error) { +func (m *MockZapiClientInterface) FlexGroupCreate(ctx context.Context, name string, size int, aggrs []azgo.AggrNameType, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int, unixGroupID int) (*azgo.VolumeCreateAsyncResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FlexGroupCreate", ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve) + ret := m.ctrl.Call(m, "FlexGroupCreate", ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, unixGroupID) ret0, _ := ret[0].(*azgo.VolumeCreateAsyncResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // FlexGroupCreate indicates an expected call of FlexGroupCreate. -func (mr *MockZapiClientInterfaceMockRecorder) FlexGroupCreate(ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve any) *gomock.Call { +func (mr *MockZapiClientInterfaceMockRecorder) FlexGroupCreate(ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, unixGroupID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlexGroupCreate", reflect.TypeOf((*MockZapiClientInterface)(nil).FlexGroupCreate), ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlexGroupCreate", reflect.TypeOf((*MockZapiClientInterface)(nil).FlexGroupCreate), ctx, name, size, aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, unixGroupID) } // FlexGroupDestroy mocks base method. @@ -2082,18 +2082,18 @@ func (mr *MockZapiClientInterfaceMockRecorder) VolumeCloneSplitStart(name any) * } // VolumeCreate mocks base method. -func (m *MockZapiClientInterface) VolumeCreate(ctx context.Context, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int, dpVolume bool) (*azgo.VolumeCreateResponse, error) { +func (m *MockZapiClientInterface) VolumeCreate(ctx context.Context, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup api.QosPolicyGroup, encrypt *bool, snapshotReserve int, dpVolume bool, unixGroupID int) (*azgo.VolumeCreateResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VolumeCreate", ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume) + ret := m.ctrl.Call(m, "VolumeCreate", ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume, unixGroupID) ret0, _ := ret[0].(*azgo.VolumeCreateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // VolumeCreate indicates an expected call of VolumeCreate. -func (mr *MockZapiClientInterfaceMockRecorder) VolumeCreate(ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume any) *gomock.Call { +func (mr *MockZapiClientInterfaceMockRecorder) VolumeCreate(ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume, unixGroupID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeCreate", reflect.TypeOf((*MockZapiClientInterface)(nil).VolumeCreate), ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeCreate", reflect.TypeOf((*MockZapiClientInterface)(nil).VolumeCreate), ctx, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, dpVolume, unixGroupID) } // VolumeDestroy mocks base method. diff --git a/storage/volume.go b/storage/volume.go index c1c67d6aa..4a1cdc014 100644 --- a/storage/volume.go +++ b/storage/volume.go @@ -31,6 +31,7 @@ type VolumeConfig struct { UnixPermissions string `json:"unixPermissions,omitempty"` StorageClass string `json:"storageClass,omitempty"` AccessMode config.AccessMode `json:"accessMode,omitempty"` + UnixGroupID string `json:"unixGroupID,omitempty"` VolumeMode config.VolumeMode `json:"volumeMode,omitempty"` AccessInfo models.VolumeAccessInfo `json:"accessInformation"` // MoveInfo is the operational status of an ongoing volume diff --git a/storage_drivers/ontap/api/abstraction_rest.go b/storage_drivers/ontap/api/abstraction_rest.go index 7a749cd0c..6693ef15c 100644 --- a/storage_drivers/ontap/api/abstraction_rest.go +++ b/storage_drivers/ontap/api/abstraction_rest.go @@ -190,9 +190,14 @@ func (d OntapAPIREST) VolumeCreate(ctx context.Context, volume Volume) (string, aggregateName = volume.Aggregates[0] } + gid, err := parseUnixGroupID(volume.UnixGroupID) + if err != nil { + return "", err + } + volumeUUID, creationErr := d.api.VolumeCreate(ctx, volume.Name, aggregateName, volume.Size, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume) + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume, gid) if creationErr != nil { return "", fmt.Errorf("error creating volume: %v", creationErr) } @@ -216,9 +221,14 @@ func (d OntapAPIREST) VolumeCreateBalanced(ctx context.Context, volume Volume) ( return "", errors.UnsupportedError("ONTAP version does not support balanced placement") } + gid, err := parseUnixGroupID(volume.UnixGroupID) + if err != nil { + return "", err + } + volumeUUID, creationErr := d.api.VolumeCreateBalanced(ctx, volume.Name, volume.Size, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume) + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume, gid) if creationErr != nil { return "", fmt.Errorf("error creating volume: %v", creationErr) } @@ -733,9 +743,14 @@ func (d OntapAPIREST) FlexgroupCreate(ctx context.Context, volume Volume) error return fmt.Errorf("%v is an invalid volume size: %v", volume.Size, err) } + gid, err := parseUnixGroupID(volume.UnixGroupID) + if err != nil { + return err + } + creationErr := d.api.FlexGroupCreate(ctx, volume.Name, volumeSize, volume.Aggregates, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, volume.TieringPolicy, - volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve) + volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, gid) if creationErr != nil { return fmt.Errorf("error creating volume: %v", creationErr) } @@ -764,9 +779,14 @@ func (d OntapAPIREST) FlexgroupCreateBalanced(ctx context.Context, volume Volume return fmt.Errorf("%v is an invalid volume size: %v", volume.Size, err) } + gid, err := parseUnixGroupID(volume.UnixGroupID) + if err != nil { + return err + } + creationErr := d.api.FlexGroupCreateBalanced(ctx, volume.Name, volumeSize, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, volume.TieringPolicy, - volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve) + volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, gid) if creationErr != nil { return fmt.Errorf("error creating volume: %v", creationErr) } diff --git a/storage_drivers/ontap/api/abstraction_rest_test.go b/storage_drivers/ontap/api/abstraction_rest_test.go index 2c782dc1e..87987a898 100644 --- a/storage_drivers/ontap/api/abstraction_rest_test.go +++ b/storage_drivers/ontap/api/abstraction_rest_test.go @@ -1570,7 +1570,8 @@ func TestVolumeCreate(t *testing.T) { // case 1: Create volume, returned No error rsi.EXPECT().VolumeCreate(ctx, volume.Name, volume.Aggregates[0], volume.Size, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume). + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume, + int64(0)). Return("", nil) _, err := oapi.VolumeCreate(ctx, volume) assert.NoError(t, err, "error returned while creating volume") @@ -1578,7 +1579,8 @@ func TestVolumeCreate(t *testing.T) { // case 2: Create volume, volume creation failed rsi.EXPECT().VolumeCreate(ctx, volume.Name, volume.Aggregates[0], volume.Size, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume). + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume, + int64(0)). Return("", errors.New("Volume create failed")) _, err = oapi.VolumeCreate(ctx, volume) assert.Error(t, err, "no error returned while creating volume") @@ -1620,7 +1622,8 @@ func TestVolumeCreateBalanced(t *testing.T) { rsi.EXPECT().SupportsFeature(ctx, api.BalancedPlacement).Return(true) rsi.EXPECT().VolumeCreateBalanced(ctx, volume.Name, volume.Size, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume). + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume, + int64(0)). Return("uuid-1", nil) volumeUUID, err := oapi.VolumeCreateBalanced(ctx, volume) assert.NoError(t, err, "error returned while creating volume balanced") @@ -1630,7 +1633,8 @@ func TestVolumeCreateBalanced(t *testing.T) { rsi.EXPECT().SupportsFeature(ctx, api.BalancedPlacement).Return(true) rsi.EXPECT().VolumeCreateBalanced(ctx, volume.Name, volume.Size, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume). + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, volume.DPVolume, + int64(0)). Return("", errors.New("Volume create balanced failed")) _, err = oapi.VolumeCreateBalanced(ctx, volume) assert.Error(t, err, "no error returned while creating volume balanced") @@ -1818,7 +1822,7 @@ func TestFlexgroupCreate(t *testing.T) { // case 1: Flexgroup create, positive test case rsi.EXPECT().FlexGroupCreate(ctx, volume.Name, 1073741824000, volume.Aggregates, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve). + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, int64(0)). Return(nil) err := oapi.FlexgroupCreate(ctx, volume) assert.NoError(t, err, "error returned while creating a flexgroup volume") @@ -1826,7 +1830,7 @@ func TestFlexgroupCreate(t *testing.T) { // case 2: Flexgroup create, negative test case rsi.EXPECT().FlexGroupCreate(ctx, volume.Name, 1073741824000, volume.Aggregates, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve). + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, int64(0)). Return(errors.New("flexgroup volume creation failed")) err = oapi.FlexgroupCreate(ctx, volume) assert.Error(t, err, "no error returned while creating a flexgroup volume") @@ -1866,7 +1870,7 @@ func TestFlexgroupCreateBalanced(t *testing.T) { rsi.EXPECT().SupportsFeature(ctx, api.BalancedPlacement).Return(true) rsi.EXPECT().FlexGroupCreateBalanced(ctx, volume.Name, 1073741824000, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve). + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, int64(0)). Return(nil) err = oapi.FlexgroupCreateBalanced(ctx, volume) assert.NoError(t, err, "error returned while creating a balanced flexgroup volume") @@ -1875,7 +1879,7 @@ func TestFlexgroupCreateBalanced(t *testing.T) { rsi.EXPECT().SupportsFeature(ctx, api.BalancedPlacement).Return(true) rsi.EXPECT().FlexGroupCreateBalanced(ctx, volume.Name, 1073741824000, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve). + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, int64(0)). Return(errors.New("flexgroup volume creation failed")) err = oapi.FlexgroupCreateBalanced(ctx, volume) assert.Error(t, err, "no error returned while creating a balanced flexgroup volume") diff --git a/storage_drivers/ontap/api/abstraction_zapi.go b/storage_drivers/ontap/api/abstraction_zapi.go index 8dab40845..461772168 100644 --- a/storage_drivers/ontap/api/abstraction_zapi.go +++ b/storage_drivers/ontap/api/abstraction_zapi.go @@ -75,13 +75,18 @@ func (d OntapAPIZAPI) VolumeCreate(ctx context.Context, volume Volume) (string, defer Logd(ctx, d.driverName, d.api.ClientConfig().DebugTraceFlags["method"]).WithFields(fields).Trace("<<<< VolumeCreate") + gid, err := parseUnixGroupIDInt(volume.UnixGroupID) + if err != nil { + return "", err + } + // ZAPI does not report a volume UUID on create, and the ZAPI path queries WAFL directly rather // than an asynchronous name index, so it is not exposed to the create/delete propagation race. // Callers therefore always delete ZAPI-created volumes by name. volCreateResponse, err := d.api.VolumeCreate(ctx, volume.Name, volume.Aggregates[0], volume.Size, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, - volume.SnapshotReserve, volume.DPVolume) + volume.SnapshotReserve, volume.DPVolume, gid) if err != nil { return "", fmt.Errorf("error creating volume: %v", err) } @@ -1363,9 +1368,14 @@ func (d OntapAPIZAPI) FlexgroupCreate(ctx context.Context, volume Volume) error return fmt.Errorf("%v is an invalid volume size: %v", volume.Size, err) } + gid, err := parseUnixGroupIDInt(volume.UnixGroupID) + if err != nil { + return err + } + flexgroupCreateResponse, err := d.api.FlexGroupCreate(ctx, volume.Name, sizeBytes, volume.Aggregates, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, - volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve) + volume.TieringPolicy, volume.Comment, volume.Qos, volume.Encrypt, volume.SnapshotReserve, gid) if err != nil { return fmt.Errorf("error creating volume: %v", err) } diff --git a/storage_drivers/ontap/api/abstraction_zapi_test.go b/storage_drivers/ontap/api/abstraction_zapi_test.go index 368deb8b1..772765b2d 100644 --- a/storage_drivers/ontap/api/abstraction_zapi_test.go +++ b/storage_drivers/ontap/api/abstraction_zapi_test.go @@ -408,7 +408,7 @@ func TestOntapAPIZAPI_VolumeCreate_Success(t *testing.T) { ctx, volume.Name, volume.Aggregates[0], volume.Size, volume.SpaceReserve, volume.SnapshotPolicy, volume.UnixPermissions, volume.ExportPolicy, volume.SecurityStyle, volume.TieringPolicy, volume.Comment, volume.Qos, - volume.Encrypt, volume.SnapshotReserve, volume.DPVolume, + volume.Encrypt, volume.SnapshotReserve, volume.DPVolume, 0, ).Return(volumeCreateResponse, nil).Times(1) _, err := oapi.VolumeCreate(ctx, volume) @@ -434,7 +434,7 @@ func TestOntapAPIZAPI_VolumeCreate_Error(t *testing.T) { mock.EXPECT().VolumeCreate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - gomock.Any()).Return(nil, errors.New("API error")).Times(1) + gomock.Any(), gomock.Any()).Return(nil, errors.New("API error")).Times(1) _, err := oapi.VolumeCreate(ctx, volume) assert.Error(t, err, "expected error when volume creation fails") @@ -467,7 +467,7 @@ func TestOntapAPIZAPI_VolumeCreate_JobExists(t *testing.T) { mock.EXPECT().VolumeCreate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - gomock.Any()).Return(volumeCreateResponse, nil).Times(1) + gomock.Any(), gomock.Any()).Return(volumeCreateResponse, nil).Times(1) _, err := oapi.VolumeCreate(ctx, volume) assert.Error(t, err, "expected VolumeCreateJobExistsError when volume create job already exists") @@ -2907,7 +2907,7 @@ func TestOntapAPIZAPI_FlexgroupCreate(t *testing.T) { mock.EXPECT().FlexGroupCreate(ctx, tt.volume.Name, sizeBytes, tt.volume.Aggregates, tt.volume.SpaceReserve, tt.volume.SnapshotPolicy, tt.volume.UnixPermissions, tt.volume.ExportPolicy, tt.volume.SecurityStyle, tt.volume.TieringPolicy, - tt.volume.Comment, tt.volume.Qos, tt.volume.Encrypt, tt.volume.SnapshotReserve). + tt.volume.Comment, tt.volume.Qos, tt.volume.Encrypt, tt.volume.SnapshotReserve, 0). Return(tt.mockResponse, tt.mockError).Times(1) } diff --git a/storage_drivers/ontap/api/fsgroup_utils.go b/storage_drivers/ontap/api/fsgroup_utils.go new file mode 100644 index 000000000..58c11f9ce --- /dev/null +++ b/storage_drivers/ontap/api/fsgroup_utils.go @@ -0,0 +1,59 @@ +// Copyright 2025 NetApp, Inc. All Rights Reserved. + +package api + +import ( + "fmt" + "strconv" +) + +const ( + // minUnixGroupID and maxUnixGroupID define the inclusive range of valid UNIX group IDs (GIDs) + // that may be applied to an ONTAP NAS volume. + minUnixGroupID = 1 + maxUnixGroupID = 4294967294 +) + +// parseUnixGroupID parses a UNIX group ID (GID) string for the REST path, returning an int64. +// An empty string is treated as "not specified" and returns 0 with no error, meaning no group ID +// is set on the volume. Any non-numeric value, or a value outside the inclusive range +// 1-4294967294, returns an error. +func parseUnixGroupID(unixGroupID string) (int64, error) { + if unixGroupID == "" { + return 0, nil + } + + gid, err := strconv.ParseInt(unixGroupID, 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid UNIX group ID %q: %w", unixGroupID, err) + } + + if gid < minUnixGroupID || gid > maxUnixGroupID { + return 0, fmt.Errorf("UNIX group ID %d is out of range (must be between %d and %d)", + gid, minUnixGroupID, maxUnixGroupID) + } + + return gid, nil +} + +// parseUnixGroupIDInt parses a UNIX group ID (GID) string for the ZAPI path, returning an int. +// An empty string is treated as "not specified" and returns 0 with no error, meaning no group ID +// is set on the volume. Any non-numeric value, or a value outside the inclusive range +// 1-4294967294, returns an error. +func parseUnixGroupIDInt(unixGroupID string) (int, error) { + if unixGroupID == "" { + return 0, nil + } + + gid, err := strconv.Atoi(unixGroupID) + if err != nil { + return 0, fmt.Errorf("invalid UNIX group ID %q: %w", unixGroupID, err) + } + + if gid < minUnixGroupID || gid > maxUnixGroupID { + return 0, fmt.Errorf("UNIX group ID %d is out of range (must be between %d and %d)", + gid, minUnixGroupID, maxUnixGroupID) + } + + return gid, nil +} diff --git a/storage_drivers/ontap/api/fsgroup_utils_test.go b/storage_drivers/ontap/api/fsgroup_utils_test.go new file mode 100644 index 000000000..93c8d13e7 --- /dev/null +++ b/storage_drivers/ontap/api/fsgroup_utils_test.go @@ -0,0 +1,75 @@ +// Copyright 2025 NetApp, Inc. All Rights Reserved. + +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseUnixGroupID(t *testing.T) { + tests := []struct { + name string + input string + expected int64 + expectError bool + }{ + {name: "empty string treated as not set", input: "", expected: 0, expectError: false}, + {name: "minimum valid GID", input: "1", expected: 1, expectError: false}, + {name: "typical GID", input: "1000", expected: 1000, expectError: false}, + {name: "maximum valid GID", input: "4294967294", expected: 4294967294, expectError: false}, + {name: "zero is out of range", input: "0", expected: 0, expectError: true}, + {name: "above maximum is out of range", input: "4294967295", expected: 0, expectError: true}, + {name: "negative is out of range", input: "-1", expected: 0, expectError: true}, + {name: "non-numeric returns error", input: "abc", expected: 0, expectError: true}, + {name: "whitespace returns error", input: " 100 ", expected: 0, expectError: true}, + {name: "float returns error", input: "100.5", expected: 0, expectError: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gid, err := parseUnixGroupID(test.input) + if test.expectError { + assert.Error(t, err, "expected an error for input %q", test.input) + assert.Equal(t, int64(0), gid, "expected zero GID on error") + } else { + assert.NoError(t, err, "unexpected error for input %q", test.input) + assert.Equal(t, test.expected, gid, "unexpected GID for input %q", test.input) + } + }) + } +} + +func TestParseUnixGroupIDInt(t *testing.T) { + tests := []struct { + name string + input string + expected int + expectError bool + }{ + {name: "empty string treated as not set", input: "", expected: 0, expectError: false}, + {name: "minimum valid GID", input: "1", expected: 1, expectError: false}, + {name: "typical GID", input: "1000", expected: 1000, expectError: false}, + {name: "maximum valid GID", input: "4294967294", expected: 4294967294, expectError: false}, + {name: "zero is out of range", input: "0", expected: 0, expectError: true}, + {name: "above maximum is out of range", input: "4294967295", expected: 0, expectError: true}, + {name: "negative is out of range", input: "-1", expected: 0, expectError: true}, + {name: "non-numeric returns error", input: "abc", expected: 0, expectError: true}, + {name: "whitespace returns error", input: " 100 ", expected: 0, expectError: true}, + {name: "float returns error", input: "100.5", expected: 0, expectError: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gid, err := parseUnixGroupIDInt(test.input) + if test.expectError { + assert.Error(t, err, "expected an error for input %q", test.input) + assert.Equal(t, 0, gid, "expected zero GID on error") + } else { + assert.NoError(t, err, "unexpected error for input %q", test.input) + assert.Equal(t, test.expected, gid, "unexpected GID for input %q", test.input) + } + }) + } +} diff --git a/storage_drivers/ontap/api/ontap_rest.go b/storage_drivers/ontap/api/ontap_rest.go index 28f1b86b5..d8871c937 100644 --- a/storage_drivers/ontap/api/ontap_rest.go +++ b/storage_drivers/ontap/api/ontap_rest.go @@ -1580,7 +1580,7 @@ func (c *RestClient) listAllVolumeNamesBackedBySnapshot(ctx context.Context, vol func (c *RestClient) createVolumeByStyle( ctx context.Context, name string, sizeInBytes int64, aggrs []string, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, - qosPolicyGroup QosPolicyGroup, encrypt *bool, snapshotReserve int, style string, dpVolume bool, + qosPolicyGroup QosPolicyGroup, encrypt *bool, snapshotReserve int, style string, dpVolume bool, unixGroupID int64, ) (string, error) { params := storage.NewVolumeCreateParamsWithTimeout(c.httpClient.Timeout) params.Context = ctx @@ -1657,6 +1657,10 @@ func (c *RestClient) createVolumeByStyle( volumeNas.ExportPolicy = &models.VolumeInlineNasInlineExportPolicy{Name: &exportPolicy} volumeInfo.Nas = volumeNas } + if unixGroupID != 0 { + volumeNas.Gid = &unixGroupID + volumeInfo.Nas = volumeNas + } params.SetInfo(volumeInfo) @@ -1706,7 +1710,7 @@ func (c *RestClient) waitForVolumeVisible(ctx context.Context, name, style strin func (c *RestClient) createApplicationContainerByStyleWithUUID( ctx context.Context, name string, sizeInBytes int64, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, - snapshotReserve int, dpVolume bool, style string, + snapshotReserve int, dpVolume bool, style string, unixGroupID int64, ) (string, error) { params := application.NewContainerCreateParamsWithTimeout(c.httpClient.Timeout) params.Context = ctx @@ -1769,6 +1773,10 @@ func (c *RestClient) createApplicationContainerByStyleWithUUID( nas.UnixPermissions = &volumePermissions haveNAS = true } + if unixGroupID != 0 { + nas.Gid = &unixGroupID + haveNAS = true + } if haveNAS { volumeInfo.Nas = nas @@ -1835,11 +1843,11 @@ func (c *RestClient) createApplicationContainerByStyleWithUUID( func (c *RestClient) createApplicationContainerByStyle( ctx context.Context, name string, sizeInBytes int64, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, - snapshotReserve int, dpVolume bool, style string, + snapshotReserve int, dpVolume bool, style string, unixGroupID int64, ) error { _, err := c.createApplicationContainerByStyleWithUUID(ctx, name, sizeInBytes, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, - snapshotReserve, dpVolume, style) + snapshotReserve, dpVolume, style, unixGroupID) return err } @@ -2010,7 +2018,7 @@ func (c *RestClient) VolumeListByAttrs( func (c *RestClient) VolumeCreate( ctx context.Context, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, - qosPolicyGroup QosPolicyGroup, encrypt *bool, snapshotReserve int, dpVolume bool, + qosPolicyGroup QosPolicyGroup, encrypt *bool, snapshotReserve int, dpVolume bool, unixGroupID int64, ) (string, error) { sizeBytesStr, err := capacity.ToBytes(size) if err != nil { @@ -2023,13 +2031,13 @@ func (c *RestClient) VolumeCreate( return c.createVolumeByStyle(ctx, name, sizeInBytes, []string{aggregateName}, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, - models.VolumeStyleFlexvol, dpVolume) + models.VolumeStyleFlexvol, dpVolume, unixGroupID) } func (c *RestClient) VolumeCreateBalanced( ctx context.Context, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, - snapshotReserve int, dpVolume bool, + snapshotReserve int, dpVolume bool, unixGroupID int64, ) (string, error) { sizeBytesStr, err := capacity.ToBytes(size) if err != nil { @@ -2042,7 +2050,7 @@ func (c *RestClient) VolumeCreateBalanced( return c.createApplicationContainerByStyleWithUUID(ctx, name, sizeInBytes, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, - snapshotReserve, dpVolume, models.VolumeStyleFlexvol) + snapshotReserve, dpVolume, models.VolumeStyleFlexvol, unixGroupID) } // VolumeModify modifies a few select Flexvol attributes @@ -5288,22 +5296,22 @@ func ToSliceVolumeAggregatesItems(aggrs []string) []*models.VolumeInlineAggregat func (c *RestClient) FlexGroupCreate( ctx context.Context, name string, size int, aggrs []string, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, - snapshotReserve int, + snapshotReserve int, unixGroupID int64, ) error { _, err := c.createVolumeByStyle(ctx, name, int64(size), aggrs, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, snapshotReserve, - models.VolumeStyleFlexgroup, false) + models.VolumeStyleFlexgroup, false, unixGroupID) return err } func (c *RestClient) FlexGroupCreateBalanced( ctx context.Context, name string, size int, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, - snapshotReserve int, + snapshotReserve int, unixGroupID int64, ) error { return c.createApplicationContainerByStyle(ctx, name, int64(size), spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment, qosPolicyGroup, encrypt, - snapshotReserve, false /*dpVolume*/, models.VolumeStyleFlexgroup) + snapshotReserve, false /*dpVolume*/, models.VolumeStyleFlexgroup, unixGroupID) } // FlexgroupModify modifies a few select Flexgroup attributes diff --git a/storage_drivers/ontap/api/ontap_rest_interface.go b/storage_drivers/ontap/api/ontap_rest_interface.go index 7ffa0949b..99346dcf3 100644 --- a/storage_drivers/ontap/api/ontap_rest_interface.go +++ b/storage_drivers/ontap/api/ontap_rest_interface.go @@ -51,12 +51,12 @@ type RestClientInterface interface { VolumeCreate( ctx context.Context, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, - snapshotReserve int, dpVolume bool, + snapshotReserve int, dpVolume bool, unixGroupID int64, ) (string, error) VolumeCreateBalanced( ctx context.Context, name, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, - snapshotReserve int, dpVolume bool, + snapshotReserve int, dpVolume bool, unixGroupID int64, ) (string, error) // VolumeModify modifies one or more volume attributes VolumeModify(ctx context.Context, volume Volume) error @@ -280,12 +280,12 @@ type RestClientInterface interface { FlexGroupCreate( ctx context.Context, name string, size int, aggrs []string, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, - snapshotReserve int, + snapshotReserve int, unixGroupID int64, ) error FlexGroupCreateBalanced( ctx context.Context, name string, size int, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, - snapshotReserve int, + snapshotReserve int, unixGroupID int64, ) error // FlexgroupModify modifies one or more volume attributes FlexgroupModify(ctx context.Context, volume Volume) error diff --git a/storage_drivers/ontap/api/ontap_rest_test.go b/storage_drivers/ontap/api/ontap_rest_test.go index ca84e97c6..fc7aaed88 100644 --- a/storage_drivers/ontap/api/ontap_rest_test.go +++ b/storage_drivers/ontap/api/ontap_rest_test.go @@ -5082,7 +5082,7 @@ func TestOntapRestCreateVolumeByStyleInvalidUnixPermission(t *testing.T) { _, err := rs.createVolumeByStyle(ctx, "fakeVolume", 1073741824, []string{"aggr1"}, "spaceReserve", "fakeSnapshotPolicy", "invalidUnixPermission", "fake-exportpolicy", "unix", "fake-tier", "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, new(true), 0, models.VolumeStyleFlexvol, - false) + false, int64(0)) assert.Error(t, err, "volume created") server.Close() } @@ -5105,7 +5105,7 @@ func TestOntapREST_VolumeCreate(t *testing.T) { _, err := rs.VolumeCreate(ctx, "fakeVolume", "aggr1", "1g", "spaceReserve", "fakeSnapshotPolicy", "---rwxr-xr-x", "fake-exportpolicy", "unix", "fake-tier", - "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, &encrypt, 0, false) + "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, &encrypt, 0, false, int64(0)) if !test.isErrorExpected { assert.NoError(t, err, "could not create a volume") } else { @@ -5151,7 +5151,7 @@ func TestOntapREST_VolumeCreateBalanced(t *testing.T) { volumeUUID, err := rs.VolumeCreateBalanced(ctx, "fakeVolume", "1g", "spaceReserve", "fakeSnapshotPolicy", "---rwxr-xr-x", "fake-exportpolicy", "unix", "fake-tier", - "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, &encrypt, 0, false) + "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, &encrypt, 0, false, int64(0)) if !test.isErrorExpected { assert.NoError(t, err, "could not create a balanced volume") assert.NotEmpty(t, volumeUUID, "balanced volume create should return the UUID from its visibility GET") @@ -5173,7 +5173,7 @@ func TestCreateApplicationContainerByStyle_Success(t *testing.T) { err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "fakeSnapshotPolicy", "---rwxr-xr-x", "fake-exportpolicy", "unix", "fake-tier", "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, &encrypt, 10, false, - models.VolumeStyleFlexvol) + models.VolumeStyleFlexvol, int64(0)) assert.NoError(t, err, "expected container create to succeed") } @@ -5187,7 +5187,7 @@ func TestCreateApplicationContainerByStyle_FlexgroupStyle(t *testing.T) { err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "fakeSnapshotPolicy", "---rwxr-xr-x", "fake-exportpolicy", "unix", "fake-tier", "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, &encrypt, 10, false, - models.VolumeStyleFlexgroup) + models.VolumeStyleFlexgroup, int64(0)) assert.NoError(t, err, "expected container create with flexgroup style to succeed") } @@ -5201,7 +5201,7 @@ func TestCreateApplicationContainerByStyle_DPVolume(t *testing.T) { err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "fakeSnapshotPolicy", "", "", "", "", "", QosPolicyGroup{Kind: InvalidQosPolicyGroupKind}, &encrypt, NumericalValueNotSet, true, - models.VolumeStyleFlexvol) + models.VolumeStyleFlexvol, int64(0)) assert.NoError(t, err, "expected container create for DP volume to succeed") } @@ -5214,7 +5214,7 @@ func TestCreateApplicationContainerByStyle_InvalidUnixPermissions(t *testing.T) err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "fakeSnapshotPolicy", "invalidUnixPermission", "fake-exportpolicy", "unix", "", "", QosPolicyGroup{Kind: InvalidQosPolicyGroupKind}, nil, NumericalValueNotSet, false, - models.VolumeStyleFlexvol) + models.VolumeStyleFlexvol, int64(0)) assert.Error(t, err, "expected error for invalid unix permissions") assert.Contains(t, err.Error(), "cannot process unix permissions") } @@ -5228,7 +5228,7 @@ func TestCreateApplicationContainerByStyle_EncryptNil(t *testing.T) { err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "fakeSnapshotPolicy", "", "", "", "", "", QosPolicyGroup{Kind: InvalidQosPolicyGroupKind}, nil, NumericalValueNotSet, false, - models.VolumeStyleFlexvol) + models.VolumeStyleFlexvol, int64(0)) assert.NoError(t, err, "expected container create with nil encrypt to succeed") } @@ -5241,7 +5241,7 @@ func TestCreateApplicationContainerByStyle_NoNASOptions(t *testing.T) { err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "", "", "", "", "", "", QosPolicyGroup{Kind: InvalidQosPolicyGroupKind}, nil, NumericalValueNotSet, false, - models.VolumeStyleFlexvol) + models.VolumeStyleFlexvol, int64(0)) assert.NoError(t, err, "expected container create with no NAS options to succeed") } @@ -5254,7 +5254,7 @@ func TestCreateApplicationContainerByStyle_WithTieringPolicy(t *testing.T) { err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "", "", "", "", "auto", "", QosPolicyGroup{Kind: InvalidQosPolicyGroupKind}, nil, NumericalValueNotSet, false, - models.VolumeStyleFlexvol) + models.VolumeStyleFlexvol, int64(0)) assert.NoError(t, err, "expected container create with tiering policy to succeed") } @@ -5267,7 +5267,7 @@ func TestCreateApplicationContainerByStyle_BackendError(t *testing.T) { err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "fakeSnapshotPolicy", "", "", "", "", "", QosPolicyGroup{Kind: InvalidQosPolicyGroupKind}, nil, NumericalValueNotSet, false, - models.VolumeStyleFlexvol) + models.VolumeStyleFlexvol, int64(0)) assert.Error(t, err, "expected error from backend") } @@ -5284,7 +5284,7 @@ func TestCreateApplicationContainerByStyle_NilAcceptedResponse(t *testing.T) { err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "", "", "", "", "", "", QosPolicyGroup{Kind: InvalidQosPolicyGroupKind}, nil, NumericalValueNotSet, false, - models.VolumeStyleFlexvol) + models.VolumeStyleFlexvol, int64(0)) assert.Error(t, err, "expected error for nil accepted response") } @@ -5297,7 +5297,7 @@ func TestCreateApplicationContainerByStyle_WithQoSPolicy(t *testing.T) { err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "", "---rwxr-xr-x", "default", "unix", "", "test comment", QosPolicyGroup{Name: "myQos", Kind: QosPolicyGroupKind}, nil, 5, false, - models.VolumeStyleFlexvol) + models.VolumeStyleFlexvol, int64(0)) assert.NoError(t, err, "expected container create with QoS policy to succeed") } @@ -5310,7 +5310,7 @@ func TestCreateApplicationContainerByStyle_WithSnapshotReserve(t *testing.T) { err := rs.createApplicationContainerByStyle(ctx, "fakeVolume", 1073741824, "none", "daily", "", "", "", "", "", QosPolicyGroup{Kind: InvalidQosPolicyGroupKind}, nil, 20, false, - models.VolumeStyleFlexvol) + models.VolumeStyleFlexvol, int64(0)) assert.NoError(t, err, "expected container create with snapshot reserve to succeed") } @@ -5739,7 +5739,7 @@ func TestOntapREST_FlexGroupCreate(t *testing.T) { err := rs.FlexGroupCreate(ctx, "fakeVolume", 1073741824, []string{"aggr1"}, "spaceReserve", "fakeSnapshotPolicy", "---rwxr-xr-x", "fake-exportpolicy", "unix", "fake-tier", - "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, new(true), 0) + "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, new(true), 0, int64(0)) if !test.isErrorExpected { assert.NoError(t, err, "could not create a flexgroup volume") } else { @@ -5768,7 +5768,7 @@ func TestOntapREST_FlexGroupCreateBalanced(t *testing.T) { err := rs.FlexGroupCreateBalanced(ctx, "fakeVolume", 1073741824, "spaceReserve", "fakeSnapshotPolicy", "---rwxr-xr-x", "fake-exportpolicy", "unix", "fake-tier", - "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, &encrypt, 0) + "comment", QosPolicyGroup{Name: "qosPolicy", Kind: QosPolicyGroupKind}, &encrypt, 0, int64(0)) if !test.isErrorExpected { assert.NoError(t, err, "could not create a balanced flexgroup volume") } else { diff --git a/storage_drivers/ontap/api/ontap_zapi.go b/storage_drivers/ontap/api/ontap_zapi.go index 7d0874f84..687134374 100644 --- a/storage_drivers/ontap/api/ontap_zapi.go +++ b/storage_drivers/ontap/api/ontap_zapi.go @@ -777,7 +777,7 @@ func (c Client) LunSize(lunPath string) (int, error) { func (c Client) FlexGroupCreate( ctx context.Context, name string, size int, aggrs []azgo.AggrNameType, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, - encrypt *bool, snapshotReserve int, + encrypt *bool, snapshotReserve int, unixGroupID int, ) (*azgo.VolumeCreateAsyncResponse, error) { junctionPath := fmt.Sprintf("/%s", name) @@ -804,6 +804,10 @@ func (c Client) FlexGroupCreate( if unixPermissions != "" { request.SetUnixPermissions(unixPermissions) } + // Set the UNIX group ownership (GID) only when specified (non-zero). + if unixGroupID != 0 { + request.SetGroupId(unixGroupID) + } // For encrypt == nil - we don't explicitely set the encrypt argument. // If destination aggregate is NAE enabled, new volume will be aggregate encrypted // else it will be volume encrypted as per Ontap's default behaviour. @@ -1179,7 +1183,7 @@ func (c Client) JobGetIterStatus(jobId int) (*azgo.JobGetIterResponse, error) { func (c Client) VolumeCreate( ctx context.Context, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, - snapshotReserve int, dpVolume bool, + snapshotReserve int, dpVolume bool, unixGroupID int, ) (*azgo.VolumeCreateResponse, error) { request := azgo.NewVolumeCreateRequest(). SetVolume(name). @@ -1208,6 +1212,11 @@ func (c Client) VolumeCreate( request.SetUnixPermissions(unixPermissions) } + // Set the UNIX group ownership (GID) only when specified (non-zero). + if unixGroupID != 0 { + request.SetGroupId(unixGroupID) + } + // Allowed ONTAP tiering Policy values // // ================================================================================= diff --git a/storage_drivers/ontap/api/ontap_zapi_interface.go b/storage_drivers/ontap/api/ontap_zapi_interface.go index 0a7a768c6..0f2546b60 100644 --- a/storage_drivers/ontap/api/ontap_zapi_interface.go +++ b/storage_drivers/ontap/api/ontap_zapi_interface.go @@ -112,7 +112,7 @@ type ZapiClientInterface interface { LunSize(lunPath string) (int, error) // FlexGroupCreate creates a FlexGroup with the specified options // equivalent to filer::> volume create -vserver svm_name -volume fg_vol_name –auto-provision-as flexgroup -size fg_size -state online -type RW -policy default -unix-permissions ---rwxr-xr-x -space-guarantee none -snapshot-policy none -security-style unix -encrypt false - FlexGroupCreate(ctx context.Context, name string, size int, aggrs []azgo.AggrNameType, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, snapshotReserve int) (*azgo.VolumeCreateAsyncResponse, error) + FlexGroupCreate(ctx context.Context, name string, size int, aggrs []azgo.AggrNameType, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, snapshotReserve int, unixGroupID int) (*azgo.VolumeCreateAsyncResponse, error) // FlexGroupDestroy destroys a FlexGroup FlexGroupDestroy(ctx context.Context, name string, force bool) (*azgo.VolumeDestroyAsyncResponse, error) // FlexGroupExists tests for the existence of a FlexGroup @@ -138,7 +138,7 @@ type ZapiClientInterface interface { JobGetIterStatus(jobId int) (*azgo.JobGetIterResponse, error) // VolumeCreate creates a volume with the specified options // equivalent to filer::> volume create -vserver iscsi_vs -volume v -aggregate aggr1 -size 1g -state online -type RW -policy default -unix-permissions ---rwxr-xr-x -space-guarantee none -snapshot-policy none -security-style unix -encrypt false - VolumeCreate(ctx context.Context, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, snapshotReserve int, dpVolume bool) (*azgo.VolumeCreateResponse, error) + VolumeCreate(ctx context.Context, name, aggregateName, size, spaceReserve, snapshotPolicy, unixPermissions, exportPolicy, securityStyle, tieringPolicy, comment string, qosPolicyGroup QosPolicyGroup, encrypt *bool, snapshotReserve int, dpVolume bool, unixGroupID int) (*azgo.VolumeCreateResponse, error) VolumeModifyExportPolicy(volumeName, exportPolicyName string) (*azgo.VolumeModifyIterResponse, error) VolumeModifyUnixPermissions(volumeName, unixPermissions string) (*azgo.VolumeModifyIterResponse, error) // VolumeCloneCreate clones a volume from a snapshot diff --git a/storage_drivers/ontap/api/ontap_zapi_test.go b/storage_drivers/ontap/api/ontap_zapi_test.go index 368c28f58..e3d577728 100644 --- a/storage_drivers/ontap/api/ontap_zapi_test.go +++ b/storage_drivers/ontap/api/ontap_zapi_test.go @@ -1353,13 +1353,13 @@ func TestZAPI_Priority1_CoreInfrastructure(t *testing.T) { // Test with all parameters volResult, err := client.VolumeCreate(ctx, "test-vol", "aggr1", "1g", "none", "default", "755", "default", "unix", "none", "test comment", - qosPolicy, new(true), 20, false) + qosPolicy, new(true), 20, false, 0) assert.NoError(t, err, "expected no error creating volume with fake server") assert.NotNil(t, volResult, "volume result should not be nil when creation succeeds") // Test with DP volume (different code path) dpResult, err := client.VolumeCreate(ctx, "dp-vol", "aggr1", "1g", "none", - "default", "", "default", "unix", "none", "", qosPolicy, nil, -1, true) + "default", "", "default", "unix", "none", "", qosPolicy, nil, -1, true, 0) assert.NoError(t, err, "expected no error creating DP volume with fake server") assert.NotNil(t, dpResult, "DP volume result should not be nil when creation succeeds") @@ -1367,7 +1367,7 @@ func TestZAPI_Priority1_CoreInfrastructure(t *testing.T) { adaptiveQos, err := NewQosPolicyGroup("", "adaptive-policy") assert.NoError(t, err, "expected no error creating adaptive QoS policy") adaptiveResult, err := client.VolumeCreate(ctx, "test-vol2", "aggr1", "1g", "none", - "default", "755", "default", "unix", "none", "", adaptiveQos, nil, -1, false) + "default", "755", "default", "unix", "none", "", adaptiveQos, nil, -1, false, 0) assert.NoError(t, err, "expected no error creating volume with adaptive QoS") assert.NotNil(t, adaptiveResult, "adaptive volume result should not be nil when creation succeeds") }) @@ -1408,13 +1408,13 @@ func TestZAPI_Priority1_FlexGroupOperations(t *testing.T) { // Test with all parameters (fixed signature) _, err := client.FlexGroupCreate(ctx, "test-flexgroup", 2, aggrList, "none", - "default", "755", "default", "unix", "none", "test comment", qosPolicy, new(true), 20) + "default", "755", "default", "unix", "none", "test comment", qosPolicy, new(true), 20, 0) assert.NoError(t, err) // Fake server supports this operation // Test with adaptive QoS (fixed signature) adaptiveQos, _ := NewQosPolicyGroup("", "adaptive-policy") _, err = client.FlexGroupCreate(ctx, "test-flexgroup2", 4, aggrList, "none", - "default", "755", "default", "unix", "backup", "", adaptiveQos, nil, -1) + "default", "755", "default", "unix", "backup", "", adaptiveQos, nil, -1, 0) assert.NoError(t, err) // Fake server supports this operation }) diff --git a/storage_drivers/ontap/api/types.go b/storage_drivers/ontap/api/types.go index bfaa25516..2ddba33a5 100644 --- a/storage_drivers/ontap/api/types.go +++ b/storage_drivers/ontap/api/types.go @@ -51,6 +51,7 @@ type Volume struct { UnixPermissions string UUID string DPVolume bool + UnixGroupID string } type ( diff --git a/storage_drivers/ontap/ontap_nas.go b/storage_drivers/ontap/ontap_nas.go index aa9aa039f..5f09c7bf5 100644 --- a/storage_drivers/ontap/ontap_nas.go +++ b/storage_drivers/ontap/ontap_nas.go @@ -439,6 +439,7 @@ func (d *NASStorageDriver) Create( SnapshotReserve: snapshotReserveInt, TieringPolicy: tieringPolicy, UnixPermissions: unixPermissions, + UnixGroupID: volConfig.UnixGroupID, DPVolume: volConfig.IsMirrorDestination, } diff --git a/storage_drivers/ontap/ontap_nas_flexgroup.go b/storage_drivers/ontap/ontap_nas_flexgroup.go index 610cce71f..71deef3b0 100644 --- a/storage_drivers/ontap/ontap_nas_flexgroup.go +++ b/storage_drivers/ontap/ontap_nas_flexgroup.go @@ -451,6 +451,7 @@ func (d *NASFlexGroupStorageDriver) Create( SnapshotReserve: snapshotReserveInt, TieringPolicy: tieringPolicy, UnixPermissions: unixPermissions, + UnixGroupID: volConfig.UnixGroupID, DPVolume: volConfig.IsMirrorDestination, }, useBalancedPlacement) if err != nil {