Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add NVMe support for block device #49

Closed
wants to merge 8 commits into from
Closed
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
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ go 1.23

require (
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.7.0
golang.org/x/sys v0.13.0
)

require gopkg.in/yaml.v3 v3.0.0-20220521103104-8f96da9f5d5e // indirect
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20220521103104-8f96da9f5d5e // indirect
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20220521103104-8f96da9f5d5e h1:3i3ny04XV6HbZ2N1oIBw1UBYATHAOpo4tfTF83JM3Z0=
Expand Down
7 changes: 6 additions & 1 deletion gofsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ var (
ErrNotImplemented = errors.New("not implemented")

// fs is the default FS instance.
fs FSinterface = &FS{ScanEntry: defaultEntryScanFunc}
fs FSinterface = &FS{ScanEntry: defaultEntryScanFunc, SysBlockDir: "/sys/block"}
)

// ContextKey is a variable containing context-keys
Expand All @@ -100,6 +100,11 @@ func UseMockFS() {
fs = &mockfs{ScanEntry: defaultEntryScanFunc}
}

// UseMockSysBlockDir creates a file system for testing.
func UseMockSysBlockDir(mockSysBlockDir string) {
fs = &FS{ScanEntry: defaultEntryScanFunc, SysBlockDir: mockSysBlockDir}
}

// GetDiskFormat uses 'lsblk' to see if the given disk is unformatted.
func GetDiskFormat(ctx context.Context, disk string) (string, error) {
return fs.GetDiskFormat(ctx, disk)
Expand Down
2 changes: 2 additions & 0 deletions gofsutil_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
type FS struct {
// ScanEntry is the function used to process mount table entries.
ScanEntry EntryScanFunc
// SysBlockDir is used to set the directory of block devices.
SysBlockDir string
}

// GetDiskFormat uses 'lsblk' to see if the given disk is unformatted.
Expand Down
12 changes: 11 additions & 1 deletion gofsutil_mount_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,17 @@ func (fs *FS) deviceRescan(_ context.Context,
if err := validatePath(path); err != nil {
return err
}
device := path + "/device/rescan"
var device string
if strings.Contains(devicePath, "nvme") {
device = path + "/device/rescan_controller"
_, err := os.Stat(device)
if os.IsNotExist(err) {
log.Warnf("This is not a valid nvme controller device %s", device)
return nil
}
} else {
device = path + "/device/rescan"
}
args := []string{"-c", "echo 1 > " + device}
log.Infof("Executing rescan command on device (%s)", devicePath)
/* #nosec G204 */
Expand Down
131 changes: 131 additions & 0 deletions gofsutil_mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ package gofsutil_test
import (
"context"
"os"
"path/filepath"
"strings"
"testing"

"github.com/dell/gofsutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestBindMount(t *testing.T) {
Expand Down Expand Up @@ -81,3 +85,130 @@ func TestGetMounts(t *testing.T) {
t.Logf("%+v", m)
}
}

func TestGetSysBlockDevicesForVolumeWWN(t *testing.T) {
tempDir := t.TempDir()
gofsutil.UseMockSysBlockDir(tempDir)

tests := []struct {
name string
wwn string
nguid string
deviceName string
deviceWwidPath []string
expect []string
errString string
}{
{
name: "iscsi block device",
wwn: "example-volume-wwn",
deviceName: "sdx",
deviceWwidPath: []string{"device", "wwid"},
expect: []string{"sdx"},
errString: "",
},
{
name: "PowerStore nvme block device",
wwn: "naa.68ccf098001111a2222b3d4444a1b23c",
nguid: "eui.1111a2222b3d44448ccf096800a1b23c",
deviceName: "nvme0n1",
deviceWwidPath: []string{"wwid"},
expect: []string{"nvme0n1"},
errString: "",
},
{
name: "PowerMax nvme block device",
wwn: "naa.60000970000120001263533030313434",
nguid: "eui.12635330303134340000976000012000",
deviceName: "nvme0n2",
deviceWwidPath: []string{"wwid"},
expect: []string{"nvme0n2"},
errString: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create the necessary directories and files
path := []string{tempDir, tt.deviceName}
path = append(path, tt.deviceWwidPath...)
deviceWwidFile := filepath.Join(path...)
err := os.MkdirAll(filepath.Dir(deviceWwidFile), 0o755)
require.Nil(t, err)
if strings.HasPrefix(tt.deviceName, "nvme") {
err = os.WriteFile(deviceWwidFile, []byte(tt.nguid), 0o600)
} else {
err = os.WriteFile(deviceWwidFile, []byte(tt.wwn), 0o600)
}
require.Nil(t, err)

// Call the function with the test input
result, err := gofsutil.GetSysBlockDevicesForVolumeWWN(context.Background(), tt.wwn)
assert.Nil(t, err)
assert.Equal(t, tt.expect, result)
})
}
}

func TestDeviceRescan(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
deviceName string
rescanFilePath []string
wantError bool
}{
{
name: "nvme device",
deviceName: "nvme0n1",
rescanFilePath: []string{"device", "rescan_controller"},
wantError: false,
},
{
name: "iscsi device",
deviceName: "sda1",
rescanFilePath: []string{"device", "rescan"},
wantError: false,
},
{
name: "invalid device",
deviceName: "invalid",
wantError: true,
},
{
name: "invalid path",
deviceName: "/",
wantError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create the necessary directories and files
path := []string{tempDir, tt.deviceName}
path = append(path, tt.rescanFilePath...)
deviceRescanFile := filepath.Join(path...)
devicePath := tt.deviceName
if !strings.Contains(tt.name, "invalid") {
devicePath = filepath.Join(tempDir, tt.deviceName)
err := os.MkdirAll(filepath.Dir(deviceRescanFile), 0o755)
require.Nil(t, err)
err = os.WriteFile(deviceRescanFile, nil, 0o600)
require.Nil(t, err)
}

// Call the function with the test input
err := gofsutil.DeviceRescan(context.Background(), devicePath)
if (err != nil) != tt.wantError {
t.Errorf("DeviceRescan() error = %v, wantError %v", err, tt.wantError)
} else if err == nil {
data, err := os.ReadFile(deviceRescanFile)
dataStr := strings.TrimSpace(string(data))
require.Nil(t, err)
if !strings.EqualFold(dataStr, "1") {
t.Errorf("DeviceRescan() file data = %s, want 1", dataStr)
}
}
})
}
}
90 changes: 82 additions & 8 deletions gofsutil_mount_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ import (
// The 'options' parameter is a list of options. Please see mount(8) for
// more information. If no options are required then please invoke Mount
// with an empty or nil argument.

// PowerMaxOUIPrefix - PowerMax format 6 OUI prefix
var PowerMaxOUIPrefix = "6000097"

// PowerStoreOUIPrefix - PowerStore format 6 OUI prefix
var PowerStoreOUIPrefix = "68ccf09"

func (fs *FS) mount(
ctx context.Context,
source, target, fsType string,
Expand Down Expand Up @@ -643,29 +650,96 @@ func (fs *FS) issueLIPToAllFCHosts(_ context.Context) error {
func (fs *FS) getSysBlockDevicesForVolumeWWN(_ context.Context, volumeWWN string) ([]string, error) {
start := time.Now()
result := make([]string, 0)
sysBlockDir := "/sys/block"
sysBlocks, err := os.ReadDir(sysBlockDir)
sysBlocks, err := os.ReadDir(fs.SysBlockDir)
if err != nil {
return result, fmt.Errorf("Error reading %s: %s", sysBlockDir, err)
return result, fmt.Errorf("Error reading %s: %s", fs.SysBlockDir, err)
}

for _, sysBlock := range sysBlocks {
name := sysBlock.Name()
if !strings.HasPrefix(name, "sd") {
// Check for both "sd" and "nvme" prefixes
if !strings.HasPrefix(name, "sd") && !strings.HasPrefix(name, "nvme") {
continue
}
wwidPath := sysBlockDir + "/" + name + "/device/wwid"

// Set the WWID path based on the device type
var wwidPath string
if strings.HasPrefix(name, "nvme") {
wwidPath = fs.SysBlockDir + "/" + name + "/wwid" // For NVMe devices
} else {
wwidPath = fs.SysBlockDir + "/" + name + "/device/wwid" // For SCSI devices
}

bytes, err := os.ReadFile(filepath.Clean(wwidPath))
if err != nil {
continue
}

wwid := strings.TrimSpace(string(bytes))
wwid = strings.Replace(wwid, "naa.", "", 1)
if wwid == volumeWWN {
result = append(result, name)

// Replace "eui." for NVMe devices and "naa." for others
if strings.HasPrefix(name, "nvme") {
wwid = strings.Replace(wwid, "eui.", "", 1)
// Use wwnMatches for NVMe comparison
if wwnMatches(wwid, volumeWWN) {
result = append(result, name)
}
} else {
wwid = strings.Replace(wwid, "naa.", "", 1)
// Compare directly for SCSI devices
if wwid == volumeWWN {
result = append(result, name)
}
}
}

end := time.Now()
dur := end.Sub(start)
log.Printf("getSysBlockDevicesForVolumeWWN %d %f", len(sysBlocks), dur.Seconds())
return result, nil
}

func wwnMatches(nguid, wwn string) bool {
/*
// PowerStore
Sample wwn : naa.68ccf098001111a2222b3d4444a1b23c
token1: 1111a2222b3d4444
token2: a1b23c
Sample nguid : 1111a2222b3d44448ccf096800a1b23c

// PowerMax
nguid: 12635330303134340000976000012000
wwn: 60000970000120001263533030313434
11aaa111111111a11a111a1111aa1111
1a111a1111aa1111 1aaa11 1 1111111a1
nguid: wwn[last16] + wwn[1:6] + wwn[0] + wwn[7:15]
1263533030313434 + 000097 + 6 + 000012000
*/
if len(wwn) < 32 {
return false
}

wwn = strings.ToLower(wwn)
if strings.HasPrefix(wwn, "naa.") {
wwn = wwn[4:]
}

var token1, token2 string
if strings.HasPrefix(wwn, PowerStoreOUIPrefix) {
token1 = wwn[13 : len(wwn)-7]
token2 = wwn[len(wwn)-6 : len(wwn)-1]
log.Infof("PowerStore: %s %s %s %t", token1, token2, nguid, strings.Contains(nguid, token2))
if strings.Contains(nguid, token1) && strings.Contains(nguid, token2) {
return true
}
} else if strings.HasPrefix(wwn, PowerMaxOUIPrefix) {
token1 = wwn[16:]
token2 = wwn[1:7]
log.Infof("Powermax: %s %s %s %t", token1, token2, nguid, strings.HasPrefix(nguid, token1+token2))
if strings.HasPrefix(nguid, token1+token2) {
return true
}
}

return false
}