From bc8581a0687e043d8b49ba2b311558aaa4d2a9d5 Mon Sep 17 00:00:00 2001 From: Evgeny Uglov Date: Mon, 26 Jan 2026 10:25:20 -0500 Subject: [PATCH] Mirror internal repository with cleaned references --- Makefile | 17 ---- README.md | 16 +++- fc.go | 25 +++-- fc_test.go | 234 ++++++++++++++++++++++++++++++++++++++++++++- go.mod | 8 +- go.sum | 16 ++-- iscsi.go | 5 +- iscsi_test.go | 153 +++++++++++++++++++++++++++++ mkdocs.yml | 7 ++ nvme.go | 13 +-- nvme_test.go | 1 + sync_tools_test.go | 2 +- 12 files changed, 447 insertions(+), 50 deletions(-) create mode 100644 mkdocs.yml diff --git a/Makefile b/Makefile index 772a270..81c88d1 100644 --- a/Makefile +++ b/Makefile @@ -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 " - @echo "" - @echo "Where '' 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" \ No newline at end of file diff --git a/README.md b/README.md index 8b2f21a..1681f14 100644 --- a/README.md +++ b/README.md @@ -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.
+For existing customers using Dell’s Container Storage Modules, you will continue to receive: +* **Ongoing Support & Community Engagement**
+ 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**
+ Deployment and update processes will remain consistent, ensuring a smooth and familiar experience. +* **Access to Documentation & Resources**
+ All documentation and related materials will remain publicly accessible, providing transparency and technical guidance. +* **Continued Access to Current Open Source Version**
+ 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.
+We deeply appreciate the contributions of the open source community and remain committed to supporting our customers through this transition.
+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** @@ -17,4 +32,3 @@ dev, err := connector.ConnectVolume(context.Background(), err = connector.DisconnectVolumeByDeviceName(context.Background(), "dm-1") ``` - diff --git a/fc.go b/fc.go index 57668d5..81399ee 100644 --- a/fc.go +++ b/fc.go @@ -451,7 +451,6 @@ func (fc *FCConnector) waitForDeviceWWN( secondsNextScan = 1 doScans := true - for doScans { var hctlsToRescan []scsi.HCTL var hctlsToDiscover []scsi.HCTL @@ -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") @@ -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 diff --git a/fc_test.go b/fc_test.go index 0d8aaf1..e70279e 100644 --- a/fc_test.go +++ b/fc_test.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "io/fs" "reflect" "testing" "time" @@ -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 @@ -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) @@ -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) + } +} diff --git a/go.mod b/go.mod index 91303ab..9759580 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 52fcc6d..4c308bc 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,10 @@ +github.com/dell/goiscsi v1.14.0 h1:kNDqOlpJ3cLSJh7Hfyn/Kz/FMCKHzV0s/xx4EqnelFw= +github.com/dell/goiscsi v1.14.0/go.mod h1:SCSC8dJCqTosU7SspaoLv6ICTKNEz08rt/I8nZ3+ptc= +github.com/dell/gonvme v1.13.0 h1:j8A1BzYA48gelih3xWd/J6LQ71CbC8Lbdyv0jG8uUNU= +github.com/dell/gonvme v1.13.0/go.mod h1:L5K7V4JZTf12m3k2wdwKwP+/eA6pr8DvlCsJU1QTGOQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dell/goiscsi v1.13.0 h1:4+uB+uJQmJ91yN7wy38sLsr5S/lqL3/tVboLOh0sg38= -github.com/dell/goiscsi v1.13.0/go.mod h1:1IPCAavfm6T9BzKS0QYfBlJz7X+AfYPYjH4G84TvJP4= -github.com/dell/gonvme v1.12.0 h1:KLOr+v+1kn/sz26CFTAkFrR1Ti4aZ37i1Mlxp1hBXYs= -github.com/dell/gonvme v1.12.0/go.mod h1:ETLwyr+OG3DYfzdlMKCv5PjeDfj+JIxV2xrbHBTg2lk= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -24,16 +24,16 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/iscsi.go b/iscsi.go index 6196a4d..1e2216d 100644 --- a/iscsi.go +++ b/iscsi.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-2026 Dell Inc. or its subsidiaries. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -156,6 +156,9 @@ type ISCSIConnector struct { type ISCSITargetInfo struct { Portal string Target string + // NetworkID is the ID of the network that this target is reachable on. + // This data is returned from the array API and filled in by the driver to manage discovery. + NetworkID string } // ISCSIVolumeInfo defines iscsi volume info diff --git a/iscsi_test.go b/iscsi_test.go index 90b87cb..7df4001 100644 --- a/iscsi_test.go +++ b/iscsi_test.go @@ -33,6 +33,7 @@ import ( wrp "github.com/dell/gobrick/internal/wrappers" "github.com/dell/goiscsi" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" "golang.org/x/sync/semaphore" "golang.org/x/sync/singleflight" ) @@ -893,3 +894,155 @@ func TestISCSIConnector_findHCTLByISCSISessionID(t *testing.T) { }) } } + +func TestAddDefaultISCSIPortToVolumeInfoPortals(t *testing.T) { + // Test case 1: When the portal doesn't contain a port + t.Run("Portal without port", func(t *testing.T) { + info := &ISCSIVolumeInfo{ + Targets: []ISCSITargetInfo{ + {Portal: "192.168.1.10", Target: "target1"}, + }, + } + + addDefaultISCSIPortToVolumeInfoPortals(info) + + assert.Equal(t, "192.168.1.10:3260", info.Targets[0].Portal) + }) + + // Test case 2: When the portal already has a port + t.Run("Portal with port", func(t *testing.T) { + info := &ISCSIVolumeInfo{ + Targets: []ISCSITargetInfo{ + {Portal: "192.168.1.10:1234", Target: "target1"}, + }, + } + + addDefaultISCSIPortToVolumeInfoPortals(info) + + assert.Equal(t, "192.168.1.10:1234", info.Targets[0].Portal) + }) + + // Test case 3: When there are multiple targets, some with and some without ports + t.Run("Multiple targets, some with and some without port", func(t *testing.T) { + info := &ISCSIVolumeInfo{ + Targets: []ISCSITargetInfo{ + {Portal: "192.168.1.10", Target: "target1"}, + {Portal: "192.168.1.20:5000", Target: "target2"}, + }, + } + + addDefaultISCSIPortToVolumeInfoPortals(info) + + assert.Equal(t, "192.168.1.10:3260", info.Targets[0].Portal) + assert.Equal(t, "192.168.1.20:5000", info.Targets[1].Portal) + }) + + // Test case 4: When the targets list is empty + t.Run("Empty targets list", func(t *testing.T) { + info := &ISCSIVolumeInfo{ + Targets: []ISCSITargetInfo{}, + } + + addDefaultISCSIPortToVolumeInfoPortals(info) + + assert.Len(t, info.Targets, 0) // Ensure the list is still empty + }) +} + +func TestValidateISCSIVolumeInfo(t *testing.T) { + connector := &ISCSIConnector{} + + // Test case 1: Empty Targets list + t.Run("Empty Targets list", func(t *testing.T) { + info := ISCSIVolumeInfo{ + Targets: []ISCSITargetInfo{}, + } + err := connector.validateISCSIVolumeInfo(context.Background(), info) + assert.EqualError(t, err, "at least one iSCSI target required") + }) + + // Test case 2: Target is empty + t.Run("Target is empty", func(t *testing.T) { + info := ISCSIVolumeInfo{ + Targets: []ISCSITargetInfo{ + {Portal: "192.168.1.10:3260", Target: ""}, + }, + } + err := connector.validateISCSIVolumeInfo(context.Background(), info) + assert.EqualError(t, err, "invalid target info") + }) + + // Test case 3: Portal is empty + t.Run("Portal is empty", func(t *testing.T) { + info := ISCSIVolumeInfo{ + Targets: []ISCSITargetInfo{ + {Portal: "", Target: "target1"}, + }, + } + err := connector.validateISCSIVolumeInfo(context.Background(), info) + assert.EqualError(t, err, "invalid target info") + }) + + // Test case 4: Both Target and Portal are valid + t.Run("Valid Target and Portal", func(t *testing.T) { + info := ISCSIVolumeInfo{ + Targets: []ISCSITargetInfo{ + {Portal: "192.168.1.10:3260", Target: "target1"}, + }, + } + err := connector.validateISCSIVolumeInfo(context.Background(), info) + assert.NoError(t, err) + }) + + // Test case 5: Multiple targets, some valid and some invalid + t.Run("Multiple targets with some invalid", func(t *testing.T) { + info := ISCSIVolumeInfo{ + Targets: []ISCSITargetInfo{ + {Portal: "192.168.1.10:3260", Target: "target1"}, + {Portal: "", Target: "target2"}, // Invalid target with empty portal + }, + } + err := connector.validateISCSIVolumeInfo(context.Background(), info) + assert.EqualError(t, err, "invalid target info") + }) +} + +func TestISCSIConnector_cleanConnection(t *testing.T) { + tests := []struct { + name string + params ISCSIConnectorParams + force bool + info ISCSIVolumeInfo + wantErr bool + }{ + { + name: "success", + params: ISCSIConnectorParams{ + Chroot: "/chroot", + }, + force: false, + info: ISCSIVolumeInfo{ + Targets: []ISCSITargetInfo{ + {Portal: "192.168.1.10:3260", Target: "target1"}, + {Portal: "192.168.1.20:3260", Target: "target2"}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewISCSIConnector(tt.params) + gotErr := c.cleanConnection(context.Background(), tt.force, tt.info) + if gotErr != nil { + if !tt.wantErr { + t.Errorf("cleanConnection() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("cleanConnection() succeeded unexpectedly") + } + }) + } +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..4a4090f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,7 @@ +site_name: 'gobrick' +site_description: 'gobrick Documentation.' +docs_dir: docs +plugins: + - techdocs-core +theme: + name: material diff --git a/nvme.go b/nvme.go index 32bf73b..8b9b5f6 100644 --- a/nvme.go +++ b/nvme.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2022 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2022-2025 Dell Inc. or its subsidiaries. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -161,8 +161,9 @@ type NVMeConnector struct { // NVMeTargetInfo - Placeholder for NVMe targets type NVMeTargetInfo struct { - Portal string - Target string + Portal string + Target string + NetworkID string } // NVMeVolumeInfo - placeholder for NVMe volume @@ -561,9 +562,7 @@ func (c *NVMeConnector) wwnMatches(nguid, wwn string) bool { } wwn = strings.ToLower(wwn) - if strings.HasPrefix(wwn, "naa.") { - wwn = wwn[4:] - } + wwn = strings.TrimPrefix(wwn, "naa.") var token1, token2 string if strings.HasPrefix(wwn, PowerStoreOUIPrefix) { @@ -580,6 +579,8 @@ func (c *NVMeConnector) wwnMatches(nguid, wwn string) bool { if strings.HasPrefix(nguid, token1+token2) { return true } + } else { + return strings.EqualFold(nguid, wwn) } return false diff --git a/nvme_test.go b/nvme_test.go index acefd88..c2c8d36 100644 --- a/nvme_test.go +++ b/nvme_test.go @@ -662,6 +662,7 @@ func TestNVME_wwnMatches(t *testing.T) { {nguid: "12635330303134340000976000012000", wwn: "68ccf070000120001263533030313434", want: false}, {nguid: "12635330303134340000976000012000", wwn: "8ccf070000120001263533030313434", want: false}, {nguid: "12635330303134340000976000012000", wwn: "68CCF070000120001263533030313434", want: false}, + {nguid: "77d3b2750000002064b94e6077518e0f", wwn: "77d3b2750000002064b94e6077518e0f", want: true}, } for _, tt := range tests { diff --git a/sync_tools_test.go b/sync_tools_test.go index 68ea273..a469999 100644 --- a/sync_tools_test.go +++ b/sync_tools_test.go @@ -2,7 +2,7 @@ /* * - * Copyright © 2020-2022 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-2025 Dell Inc. or its subsidiaries. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.