Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ Create and apply StorageClasses with the properties you want.
| *formatOptions* | string | Additional options/arguments passed to `mkfs.*` command. See a linux manual that corresponds with your FS of choice. | - | iSCSI |
| *enableSpaceReclamation* | string | Enables space reclamation for Thin Provisioned Btrfs LUNs to improve storage efficiency. May impact performance and space display. | 'false' | iSCSI |
| *enableFuaSyncCache* | string | Enables FUA and Sync Cache SCSI commands for LUNs. | 'false' | iSCSI |
| *allowMultipleSessions* | string | Enables multiple iSCSI sessions to the target from one or more initiators. Required for PVC remount after node restarts. | 'false' | iSCSI |
| *csi.storage.k8s.io/node-stage-secret-name* | string | The name of node-stage-secret. Required if DSM shared folder is accessed via SMB. | - | SMB |
| *csi.storage.k8s.io/node-stage-secret-namespace* | string | The namespace of node-stage-secret. Required if DSM shared folder is accessed via SMB. | - | SMB |
| *mountPermissions* | string | Mounted folder permissions. If set as non-zero, driver will perform `chmod` after mount | '0750' | NFS |
Expand Down
13 changes: 13 additions & 0 deletions pkg/driver/controllerserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,19 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
return nil, status.Errorf(codes.InvalidArgument, "Unsupported nfsvers: %s", nfsVer)
}

// Parse allowMultipleSessions parameter for iSCSI targets
if params["allowMultipleSessions"] != "" {
allowMultipleSessions := utils.StringToBoolean(params["allowMultipleSessions"])
if protocol != utils.ProtocolIscsi {
log.Infof("Volume [%s]: allowMultipleSessions parameter ignored for protocol [%s]", volName, protocol)
} else if allowMultipleSessions && !multiSession {
log.Infof("Enabling multi-session for volume [%s] via allowMultipleSessions parameter", volName)
multiSession = true

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was mentioned in the discussion of ticket 109. I simply had Claude help me through implementing it in a clear way.
I'm actually running this version on my NAS and have had k3s and microk8s quite happy with it for a week and change, so I'm happy so far.

} else if !allowMultipleSessions && multiSession {
log.Warnf("Volume [%s]: allowMultipleSessions=false but access mode requires multi-session; using multi-session", volName)
}
}

spec := &models.CreateK8sVolumeSpec{
DsmIp: params["dsm"],
K8sVolumeName: volName,
Expand Down
324 changes: 324 additions & 0 deletions pkg/driver/controllerserver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
/*
Copyright 2021 Synology Inc.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a few years between friends?


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 driver

import (
"context"
"testing"

"github.com/container-storage-interface/spec/lib/go/csi"

"github.com/SynologyOpenSource/synology-csi/pkg/dsm/common"
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/webapi"
"github.com/SynologyOpenSource/synology-csi/pkg/interfaces"
"github.com/SynologyOpenSource/synology-csi/pkg/models"
)

// mockDsmService implements interfaces.IDsmService for testing
type mockDsmService struct {
createVolumeFunc func(spec *models.CreateK8sVolumeSpec) (*models.K8sVolumeRespSpec, error)
getVolumeByName *models.K8sVolumeRespSpec
}

func (m *mockDsmService) AddDsm(client common.ClientInfo) error { return nil }
func (m *mockDsmService) RemoveAllDsms() {}
func (m *mockDsmService) GetDsm(ip string) (*webapi.DSM, error) { return nil, nil }
func (m *mockDsmService) GetDsmsCount() int { return 1 }
func (m *mockDsmService) ListDsmVolumes(ip string) ([]webapi.VolInfo, error) {
return nil, nil
}
func (m *mockDsmService) CreateVolume(spec *models.CreateK8sVolumeSpec) (*models.K8sVolumeRespSpec, error) {
if m.createVolumeFunc != nil {
return m.createVolumeFunc(spec)
}
return &models.K8sVolumeRespSpec{
VolumeId: "test-volume-id",
SizeInBytes: spec.Size,
Protocol: spec.Protocol,
}, nil
}
func (m *mockDsmService) DeleteVolume(volId string) error { return nil }
func (m *mockDsmService) ListVolumes() []*models.K8sVolumeRespSpec { return nil }
func (m *mockDsmService) GetVolume(volId string) *models.K8sVolumeRespSpec { return nil }
func (m *mockDsmService) ExpandVolume(volId string, newSize int64) (*models.K8sVolumeRespSpec, error) {
return nil, nil
}
func (m *mockDsmService) CreateSnapshot(spec *models.CreateK8sVolumeSnapshotSpec) (*models.K8sSnapshotRespSpec, error) {
return nil, nil
}
func (m *mockDsmService) DeleteSnapshot(snapshotUuid string) error { return nil }
func (m *mockDsmService) ListAllSnapshots() []*models.K8sSnapshotRespSpec { return nil }
func (m *mockDsmService) ListSnapshots(volId string) []*models.K8sSnapshotRespSpec { return nil }
func (m *mockDsmService) GetVolumeByName(volName string) *models.K8sVolumeRespSpec {
return m.getVolumeByName
}
func (m *mockDsmService) GetSnapshotByName(snapshotName string) *models.K8sSnapshotRespSpec {
return nil
}

func newTestControllerServer(dsmService interfaces.IDsmService) *controllerServer {
d := &Driver{
name: DriverName,
version: DriverVersion,
}
d.addVolumeCapabilityAccessModes([]csi.VolumeCapability_AccessMode_Mode{
csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
})

return &controllerServer{
Driver: d,
dsmService: dsmService,
}
}

func TestCreateVolume_AllowMultipleSessions(t *testing.T) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was particularly stern with Claude that we must have tests for the new functionality. They mostly center on configuration, but as really the change is mostly about configuration, I shouldn't be surprised.

tests := []struct {
name string
params map[string]string
accessMode csi.VolumeCapability_AccessMode_Mode
wantMultipleSession bool
}{
{
name: "allowMultipleSessions true with SINGLE_NODE_WRITER",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
"allowMultipleSessions": "true",
},
accessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
wantMultipleSession: true,
},
{
name: "allowMultipleSessions false with SINGLE_NODE_WRITER",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
"allowMultipleSessions": "false",
},
accessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
wantMultipleSession: false,
},
{
name: "allowMultipleSessions not set with SINGLE_NODE_WRITER",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
},
accessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
wantMultipleSession: false,
},
{
name: "allowMultipleSessions true with MULTI_NODE_MULTI_WRITER",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
"allowMultipleSessions": "true",
},
accessMode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
wantMultipleSession: true,
},
{
name: "allowMultipleSessions false with MULTI_NODE_MULTI_WRITER - access mode wins",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
"allowMultipleSessions": "false",
},
accessMode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
wantMultipleSession: true,
},
{
name: "allowMultipleSessions not set with MULTI_NODE_MULTI_WRITER",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
},
accessMode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
wantMultipleSession: true,
},
{
name: "allowMultipleSessions yes (alternative boolean)",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
"allowMultipleSessions": "yes",
},
accessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
wantMultipleSession: true,
},
{
name: "allowMultipleSessions 1 (alternative boolean)",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
"allowMultipleSessions": "1",
},
accessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
wantMultipleSession: true,
},
{
name: "allowMultipleSessions TRUE (case insensitive)",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
"allowMultipleSessions": "TRUE",
},
accessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
wantMultipleSession: true,
},
{
name: "allowMultipleSessions empty string",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
"allowMultipleSessions": "",
},
accessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
wantMultipleSession: false,
},
{
name: "allowMultipleSessions invalid value",
params: map[string]string{
"protocol": "iscsi",
"location": "/volume1",
"allowMultipleSessions": "invalid",
},
accessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
wantMultipleSession: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var capturedSpec *models.CreateK8sVolumeSpec
mock := &mockDsmService{
createVolumeFunc: func(spec *models.CreateK8sVolumeSpec) (*models.K8sVolumeRespSpec, error) {
capturedSpec = spec
return &models.K8sVolumeRespSpec{
VolumeId: "test-volume-id",
SizeInBytes: spec.Size,
Protocol: spec.Protocol,
}, nil
},
}

cs := newTestControllerServer(mock)

req := &csi.CreateVolumeRequest{
Name: "test-volume",
CapacityRange: &csi.CapacityRange{
RequiredBytes: 1 * 1024 * 1024 * 1024, // 1GB
},
VolumeCapabilities: []*csi.VolumeCapability{
{
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: tt.accessMode,
},
AccessType: &csi.VolumeCapability_Mount{
Mount: &csi.VolumeCapability_MountVolume{},
},
},
},
Parameters: tt.params,
}

_, err := cs.CreateVolume(context.Background(), req)
if err != nil {
t.Fatalf("CreateVolume failed: %v", err)
}

if capturedSpec == nil {
t.Fatal("CreateVolume did not call dsmService.CreateVolume")
}

if capturedSpec.MultipleSession != tt.wantMultipleSession {
t.Errorf("MultipleSession = %v, want %v", capturedSpec.MultipleSession, tt.wantMultipleSession)
}
})
}
}

func TestCreateVolume_AllowMultipleSessions_NonIscsi(t *testing.T) {
// Test that allowMultipleSessions is ignored for non-iSCSI protocols
tests := []struct {
name string
protocol string
}{
{
name: "SMB protocol ignores allowMultipleSessions",
protocol: "smb",
},
{
name: "NFS protocol ignores allowMultipleSessions",
protocol: "nfs",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var capturedSpec *models.CreateK8sVolumeSpec
mock := &mockDsmService{
createVolumeFunc: func(spec *models.CreateK8sVolumeSpec) (*models.K8sVolumeRespSpec, error) {
capturedSpec = spec
return &models.K8sVolumeRespSpec{
VolumeId: "test-volume-id",
SizeInBytes: spec.Size,
Protocol: spec.Protocol,
}, nil
},
}

cs := newTestControllerServer(mock)

req := &csi.CreateVolumeRequest{
Name: "test-volume",
CapacityRange: &csi.CapacityRange{
RequiredBytes: 1 * 1024 * 1024 * 1024, // 1GB
},
VolumeCapabilities: []*csi.VolumeCapability{
{
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
},
AccessType: &csi.VolumeCapability_Mount{
Mount: &csi.VolumeCapability_MountVolume{},
},
},
},
Parameters: map[string]string{
"protocol": tt.protocol,
"location": "/volume1",
"allowMultipleSessions": "true",
},
}

_, err := cs.CreateVolume(context.Background(), req)
if err != nil {
t.Fatalf("CreateVolume failed: %v", err)
}

if capturedSpec == nil {
t.Fatal("CreateVolume did not call dsmService.CreateVolume")
}

// For non-iSCSI protocols, MultipleSession should still be set
// based on access mode, but it's not used by the share volume logic.
// The parameter should not cause an error.
})
}
}
2 changes: 1 addition & 1 deletion pkg/driver/multipath.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (t *tools) execWithTimeout(command string, args []string, timeout time.Dura
if err != nil {
if ee, ok := err.(utilexec.ExitError); ok {
log.Errorf("Non-zero exit code: %s", err)
err = fmt.Errorf("%s", ee.ExitStatus())
err = fmt.Errorf("%d", ee.ExitStatus())

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These other fixes were simply because my linter would not let it go.

}
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/driver/nodeserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func (ns *nodeServer) getPortals(dsmIp string) []string {
if dsm.IsUC() && ns.tools.IsMultipathEnabled() {
dsm2, err := dsm.GetAnotherController()
if err != nil {
log.Errorf("[%s] UC failed to get another controller: %v", err)
log.Errorf("[%s] UC failed to get another controller: %v", dsmIp, err)
} else {
portals = append(portals, fmt.Sprintf("%s:%d", dsm2.Ip, ISCSIPort))
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/dsm/webapi/iscsi.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func errCodeMapping(errCode int, oriErr error) error {
}

if errCode > 18990000 {
return utils.IscsiDefaultError{errCode}
return utils.IscsiDefaultError{ErrCode: errCode}
}
return oriErr
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/dsm/webapi/share.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func shareErrCodeMapping(errCode int, oriErr error) error {
}

if errCode >= 3300 {
return utils.ShareDefaultError{errCode}
return utils.ShareDefaultError{ErrCode: errCode}
}
return oriErr
}
Expand Down
3 changes: 2 additions & 1 deletion test/sanity/sanity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ func TestSanity(t *testing.T) {

// Set Input parameters for test
testConfig.TestVolumeParameters = map[string]string{
"protocol": "smb",
"protocol": "iscsi",
"allowMultipleSessions": "true",
}

// testConfig.TestVolumeAccessType = "block" // raw block
Expand Down