Skip to content
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
17 changes: 0 additions & 17 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,3 @@ check:

gocover:
go tool cover -html=c.out

.PHONY: actions action-help
actions: ## Run all GitHub Action checks that run on a pull request creation
@echo "Running all GitHub Action checks for pull request events..."
@act -l | grep -v ^Stage | grep pull_request | grep -v image_security_scan | awk '{print $$2}' | while read WF; do \
echo "Running workflow: $${WF}"; \
act pull_request --no-cache-server --platform ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest --job "$${WF}"; \
done

action-help: ## Echo instructions to run one specific workflow locally
@echo "GitHub Workflows can be run locally with the following command:"
@echo "act pull_request --no-cache-server --platform ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest --job <jobid>"
@echo ""
@echo "Where '<jobid>' is a Job ID returned by the command:"
@echo "act -l"
@echo ""
@echo "NOTE: if act is not installed, it can be downloaded from https://github.com/nektos/act"
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# :lock: **Important Notice**
Starting with the release of **Container Storage Modules v1.16.0**, this repository will no longer be maintained as an open source project. Future development will continue under a closed source model. This change reflects our commitment to delivering even greater value to our customers by enabling faster innovation and more deeply integrated features with the Dell storage portfolio.<br>
For existing customers using Dell’s Container Storage Modules, you will continue to receive:
* **Ongoing Support & Community Engagement**<br>
You will continue to receive high-quality support through Dell Support and our community channels. Your experience of engaging with the Dell community remains unchanged.
* **Streamlined Deployment & Updates**<br>
Deployment and update processes will remain consistent, ensuring a smooth and familiar experience.
* **Access to Documentation & Resources**<br>
All documentation and related materials will remain publicly accessible, providing transparency and technical guidance.
* **Continued Access to Current Open Source Version**<br>
The current open-source version will remain available under its existing license for those who rely on it.

Moving to a closed source model allows Dell’s development team to accelerate feature delivery and enhance integration across our Enterprise Kubernetes Storage solutions ultimately providing a more seamless and robust experience.<br>
We deeply appreciate the contributions of the open source community and remain committed to supporting our customers through this transition.<br>
For questions or access requests, please contact the maintainers via [Dell Support](https://www.dell.com/support/kbdoc/en-in/000188046/container-storage-interface-csi-drivers-and-container-storage-modules-csm-how-to-get-support).

# GOBRICK
**Library for iSCSI/FC/NVMe volume connection**
Expand All @@ -17,4 +32,3 @@ dev, err := connector.ConnectVolume(context.Background(),
err = connector.DisconnectVolumeByDeviceName(context.Background(), "dm-1")
```


25 changes: 15 additions & 10 deletions fc.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,6 @@ func (fc *FCConnector) waitForDeviceWWN(
secondsNextScan = 1

doScans := true

for doScans {
var hctlsToRescan []scsi.HCTL
var hctlsToDiscover []scsi.HCTL
Expand All @@ -474,6 +473,8 @@ func (fc *FCConnector) waitForDeviceWWN(
}
secondsNextScan = int(math.Pow(float64(numRescans+2), 2))
}
var devicesToValidate []string
var validDevices []string
for _, hctl := range hctlsToDiscover {
if !hctl.IsFullInfo() {
logger.Debug(ctx, "HCTL incomplete, skip device resolving")
Expand All @@ -493,19 +494,23 @@ func (fc *FCConnector) waitForDeviceWWN(
logger.Error(ctx, err.Error())
}
}

devicesToValidate = append(devicesToValidate, d)
}
for _, d := range devicesToValidate {
if fc.scsi.CheckDeviceIsValid(ctx, path.Join("/dev/", d)) {
logger.Debug(ctx, "device %s is valid", d)
wwn, err := fc.scsi.GetDeviceWWN(ctx, []string{d})
if err != nil {
logger.Error(ctx, "failed to get %s WWN: %s", d, err.Error())
continue
}
logger.Info(ctx, "FC wwn found: %s", wwn)
return wwn, nil
validDevices = append(validDevices, d)
}
logger.Debug(ctx, "device %s is invalid", d)
}
if len(validDevices) > 0 {
wwn, err := fc.scsi.GetDeviceWWN(ctx, validDevices)
if err != nil {
return "", err
}
logger.Info(ctx, "wwn for FC device found: %s", wwn)
return wwn, nil
}

select {
case <-ctx.Done():
doScans = false
Expand Down
234 changes: 232 additions & 2 deletions fc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"io/fs"
"reflect"
"testing"
"time"
Expand Down Expand Up @@ -61,6 +62,15 @@ type fcFields struct {
waitDeviceRegisterTimeout time.Duration
}

type fakeDirFileInfo struct{ name string }

func (f fakeDirFileInfo) Name() string { return f.name }
func (f fakeDirFileInfo) Size() int64 { return 0 }
func (f fakeDirFileInfo) Mode() fs.FileMode { return fs.ModeDir }
func (f fakeDirFileInfo) ModTime() time.Time { return time.Time{} }
func (f fakeDirFileInfo) IsDir() bool { return true }
func (f fakeDirFileInfo) Sys() any { return nil }

func getDefaultFCFields(ctrl *gomock.Controller) fcFields {
con := NewFCConnector(FCConnectorParams{})
bc := con.baseConnector
Expand Down Expand Up @@ -123,21 +133,29 @@ func waitForDeviceWWNMock(mock *baseMockHelper,
) {
findHCTLsForFCHBAMock(mock, filepath, os)

// First round: all return error
mock.SCSIGetDeviceNameByHCTLCallH = validHCTL1
mock.SCSIGetDeviceNameByHCTLErr(scsi)

mock.SCSIGetDeviceNameByHCTLCallH = validHCTL2
mock.SCSIGetDeviceNameByHCTLErr(scsi)

mock.SCSIGetDeviceNameByHCTLCallH = validHCTL1Target1
mock.SCSIGetDeviceNameByHCTLErr(scsi)

// Simulate re-scan
findHCTLsForFCHBAMock(mock, filepath, os)

// Second round
mock.SCSIGetDeviceNameByHCTLCallH = validHCTL1
mock.SCSIGetDeviceNameByHCTLOKReturn = mockhelper.ValidDeviceName
mock.SCSIGetDeviceNameByHCTLOK(scsi)

mock.SCSIGetDeviceNameByHCTLCallH = validHCTL2
mock.SCSIGetDeviceNameByHCTLErr(scsi)

mock.SCSIGetDeviceNameByHCTLCallH = validHCTL1Target1
mock.SCSIGetDeviceNameByHCTLErr(scsi)

// Devices to validate
mock.SCSICheckDeviceIsValidCallDevice = mockhelper.ValidDevicePath
mock.SCSICheckDeviceIsValidOKReturn = true
mock.SCSICheckDeviceIsValidOK(scsi)
Expand Down Expand Up @@ -1127,3 +1145,215 @@ func TestFCConnector_findHCTLsForFCHBA(t *testing.T) {
})
}
}

func TestFCConnector_DisconnectVolumeByWWN_AcquireFails(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

fields := getDefaultFCFields(ctrl)
if fields.limiter == nil {
fields.limiter = semaphore.NewWeighted(1)
}

ctx, cancel := context.WithCancel(context.Background())
cancel()

fc := &FCConnector{
baseConnector: fields.baseConnector,
multipath: fields.multipath,
scsi: fields.scsi,
filePath: fields.filePath,
os: fields.os,
limiter: fields.limiter,
waitDeviceRegisterTimeout: fields.waitDeviceRegisterTimeout,
}

err := fc.DisconnectVolumeByWWN(ctx, "any-wwn")
if err == nil {
t.Fatalf("expected limiter error, got nil")
}
want := "too many parallel operations. try later"
if err.Error() != want {
t.Fatalf("error = %q, want %q", err.Error(), want)
}
}

func TestFCConnector_DisconnectVolumeByDeviceName_AcquireFails(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

fields := getDefaultFCFields(ctrl)
if fields.limiter == nil {
fields.limiter = semaphore.NewWeighted(1)
}

ctx, cancel := context.WithCancel(context.Background())
cancel()

fc := &FCConnector{
baseConnector: fields.baseConnector,
multipath: fields.multipath,
scsi: fields.scsi,
filePath: fields.filePath,
os: fields.os,
limiter: fields.limiter,
waitDeviceRegisterTimeout: fields.waitDeviceRegisterTimeout,
}

err := fc.DisconnectVolumeByDeviceName(ctx, "device-name")
if err == nil {
t.Fatalf("expected limiter error, got nil")
}
want := "too many parallel operations. try later"
if err.Error() != want {
t.Fatalf("error = %q, want %q", err.Error(), want)
}
}

func TestFCConnector_cleanConnection_GetFCHBASInfoError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

fields := getDefaultFCFields(ctrl)

scanErr := errors.New("scan error")

// Allow the initial Stat() probe to pass
fields.os.EXPECT().
Stat("/sys/class/fc_host").
Return(fakeDirFileInfo{"fc_host"}, nil).
AnyTimes()

// Force the HBA enumeration to fail
fields.filePath.EXPECT().
Glob(gomock.Any()).
Return(nil, scanErr).
AnyTimes()

fc := &FCConnector{
baseConnector: fields.baseConnector,
multipath: fields.multipath,
scsi: fields.scsi,
filePath: fields.filePath,
os: fields.os,
limiter: fields.limiter,
waitDeviceRegisterTimeout: fields.waitDeviceRegisterTimeout,
}

err := fc.cleanConnection(context.Background(), true, FCVolumeInfo{})
if err == nil {
t.Fatalf("cleanConnection() expected error, got nil")
}
if !errors.Is(err, scanErr) {
t.Fatalf("cleanConnection() error = %v, want %v", err, scanErr)
}
}

func stubWaitUdev(t *testing.T, fn func(context.Context, *FCConnector) func(context.Context, string, string) error) {
t.Helper()
orig := waitUdevSymlinkFunc
waitUdevSymlinkFunc = fn
t.Cleanup(func() { waitUdevSymlinkFunc = orig })
}

func Test_waitSingleDevice_Success_FirstIteration_NoSleep(t *testing.T) {
ctx := context.Background()
wwn := "wwn-123"
devices := []string{"sda", "sdb"}

stubWaitUdev(t, func(_ context.Context, _ *FCConnector) func(context.Context, string, string) error {
return func(_ context.Context, d string, gotWWN string) error {
if d == "sdb" && gotWWN == wwn {
return nil
}
return errors.New("not ready")
}
})

fc := &FCConnector{waitDeviceRegisterTimeout: 1 * time.Second}

got, err := fc.waitSingleDevice(ctx, wwn, devices)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "sdb" {
t.Fatalf("got device %q, want %q", got, "sdb")
}
}

func Test_waitSingleDevice_CanceledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()

stubWaitUdev(t, func(_ context.Context, _ *FCConnector) func(context.Context, string, string) error {
return func(_ context.Context, _ string, _ string) error { return errors.New("should not be called") }
})

fc := &FCConnector{waitDeviceRegisterTimeout: 1 * time.Second}

got, err := fc.waitSingleDevice(ctx, "wwn-xyz", []string{"sda"})
if err == nil || err.Error() != "waitDevice canceled" {
t.Fatalf("expected 'waitDevice canceled' error, got %v", err)
}
if got != "" {
t.Fatalf("expected empty device on cancel, got %q", got)
}
}

func TestFCConnector_DisconnectVolume_AcquireFails_CanceledCtx(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

fields := getDefaultFCFields(ctrl)
if fields.limiter == nil {
fields.limiter = semaphore.NewWeighted(1)
}

fc := &FCConnector{
baseConnector: fields.baseConnector,
multipath: fields.multipath,
scsi: fields.scsi,
filePath: fields.filePath,
os: fields.os,
limiter: fields.limiter,
waitDeviceRegisterTimeout: fields.waitDeviceRegisterTimeout,
}

ctx, cancel := context.WithCancel(context.Background())
cancel()

err := fc.DisconnectVolume(ctx, validFCVolumeInfo)
if err == nil {
t.Fatalf("expected limiter error, got nil")
}
want := "too many parallel operations. try later"
if err.Error() != want {
t.Fatalf("error = %q, want %q", err.Error(), want)
}
}

func TestFCConnector_connectDevice_ErrorPath_Simple(t *testing.T) {
ctx := context.Background()
fc := &FCConnector{}

origTrace := traceFuncCallFunc
traceFuncCallFunc = func(_ context.Context, _ string) func() { return func() {} }
t.Cleanup(func() { traceFuncCallFunc = origTrace })

wantErr := errors.New("wwn lookup failed")
origWait := waitForDeviceWWNFunc
waitForDeviceWWNFunc = func(_ context.Context, _ *FCConnector) func(context.Context, []FCHBA, FCVolumeInfo) (string, error) {
return func(context.Context, []FCHBA, FCVolumeInfo) (string, error) {
return "", wantErr
}
}
t.Cleanup(func() { waitForDeviceWWNFunc = origWait })

dev, err := fc.connectDevice(ctx, []FCHBA{{}}, validFCVolumeInfo)
if err == nil || err != wantErr {
t.Fatalf("connectDevice() error = %v, want %v", err, wantErr)
}
if dev != (Device{}) {
t.Fatalf("connectDevice() dev = %#v, want zero Device{}", dev)
}
}
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ module github.com/dell/gobrick
go 1.25

require (
github.com/dell/goiscsi v1.13.0
github.com/dell/gonvme v1.12.0
github.com/dell/goiscsi v1.14.0
github.com/dell/gonvme v1.13.0
github.com/golang/mock v1.6.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.11.0
golang.org/x/sync v0.17.0
golang.org/x/sync v0.19.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/sys v0.38.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading