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
15 changes: 13 additions & 2 deletions pkg/initiator/nvmecli.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
commonns "github.com/longhorn/go-common-libs/ns"

"github.com/longhorn/go-spdk-helper/pkg/types"

spdkutil "github.com/longhorn/go-spdk-helper/pkg/util"
)

const (
Expand Down Expand Up @@ -234,9 +236,13 @@ func getHostID(executor *commonns.Executor) (string, error) {
}

func discovery(hostID, hostNQN, ip, port string, executor *commonns.Executor) ([]DiscoveryPageEntry, error) {
ip = spdkutil.NormalizeNvmeAddr(ip)

opts := []string{
"discover",
"-t", DefaultTransportType,
// nvme-cli -a accepts bare IPv6 (no brackets). net.SplitHostPort callers
// upstream strip brackets; util.NormalizeNvmeAddr is a safety net.
"-a", ip,
"-s", port,
"-o", "json",
Expand Down Expand Up @@ -303,6 +309,8 @@ func discovery(hostID, hostNQN, ip, port string, executor *commonns.Executor) ([
}

func connect(hostID, hostNQN, nqn, transpotType, ip, port string, executor *commonns.Executor) (string, error) {
ip = spdkutil.NormalizeNvmeAddr(ip)

var err error

opts := []string{
Expand All @@ -322,6 +330,8 @@ func connect(hostID, hostNQN, nqn, transpotType, ip, port string, executor *comm
opts = append(opts, "-q", hostNQN)
}
if ip != "" {
// nvme-cli -a accepts bare IPv6 (no brackets). net.SplitHostPort callers
// upstream strip brackets; util.NormalizeNvmeAddr is a safety net.
opts = append(opts, "-a", ip)
}
if port != "" {
Expand Down Expand Up @@ -402,8 +412,9 @@ func extractJSONString(str string) (string, error) {
return "", fmt.Errorf("invalid JSON string")
}

// GetIPAndPortFromControllerAddress returns the IP and port from the controller address
// GetIPAndPortFromControllerAddress returns the IP and port from the controller address.
// Input can be either "traddr=10.42.2.18 trsvcid=20006" or "traddr=10.42.2.18,trsvcid=20006"
// for IPv4, or "traddr=fd00::1 trsvcid=20006" for IPv6 (traddr may contain colons).
func GetIPAndPortFromControllerAddress(address string) (string, string) {
var traddr, trsvcid string

Expand All @@ -412,7 +423,7 @@ func GetIPAndPortFromControllerAddress(address string) (string, string) {
})

for _, part := range parts {
keyVal := strings.Split(part, "=")
keyVal := strings.SplitN(part, "=", 2)
Comment thread
derekbit marked this conversation as resolved.
if len(keyVal) == 2 {
key := strings.TrimSpace(keyVal[0])
value := strings.TrimSpace(keyVal[1])
Expand Down
31 changes: 31 additions & 0 deletions pkg/initiator/nvmecli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package initiator

import (
"testing"
)

func TestGetIPAndPortFromControllerAddressIPv6(t *testing.T) {
testCases := []struct {
name string
address string
expectedIP string
expectedPort string
}{
{"IPv6 space-separated", "traddr=fd00::1 trsvcid=20001", "fd00::1", "20001"},
{"IPv6 comma-separated", "traddr=fd00::1,trsvcid=20001", "fd00::1", "20001"},
{"IPv6 loopback", "traddr=::1 trsvcid=4420", "::1", "4420"},
{"IPv4 space-separated", "traddr=10.42.2.18 trsvcid=20006", "10.42.2.18", "20006"},
{"IPv4 comma-separated", "traddr=10.42.2.18,trsvcid=20006", "10.42.2.18", "20006"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotIP, gotPort := GetIPAndPortFromControllerAddress(tc.address)
if gotIP != tc.expectedIP {
t.Errorf("GetIPAndPortFromControllerAddress(%q) IP = %q, want %q", tc.address, gotIP, tc.expectedIP)
}
if gotPort != tc.expectedPort {
t.Errorf("GetIPAndPortFromControllerAddress(%q) Port = %q, want %q", tc.address, gotPort, tc.expectedPort)
}
})
}
}
40 changes: 35 additions & 5 deletions pkg/spdk/client/advanced.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package client

import (
"net"
"path/filepath"

"github.com/sirupsen/logrus"

"github.com/longhorn/go-spdk-helper/pkg/jsonrpc"

spdktypes "github.com/longhorn/go-spdk-helper/pkg/spdk/types"
spdkutil "github.com/longhorn/go-spdk-helper/pkg/util"
)

// AddDevice adds a device with the given device path, name, and cluster size.
Expand Down Expand Up @@ -55,8 +57,30 @@ func (c *Client) DeleteDevice(bdevAioName, lvsName string) (err error) {
return nil
}

// DetectAddressFamily returns the NVMe address family for the given IP.
// Exported so downstream repos (longhorn-spdk-engine) can reuse it.
func DetectAddressFamily(ip string) spdktypes.NvmeAddressFamily {
if ip == "" {
return spdktypes.NvmeAddressFamilyIPv4
}

normalized := spdkutil.NormalizeNvmeAddr(ip)
parsedIP := net.ParseIP(normalized)
if parsedIP == nil {
logrus.Warnf("Failed to parse IP %q, defaulting to IPv4", ip)
return spdktypes.NvmeAddressFamilyIPv4
}

if parsedIP.To4() == nil {
return spdktypes.NvmeAddressFamilyIPv6
}
return spdktypes.NvmeAddressFamilyIPv4
Comment thread
COLDTURNIP marked this conversation as resolved.
}

Comment thread
COLDTURNIP marked this conversation as resolved.
// StartExposeBdev exposes the bdev with the given nqn, bdevName, nguid, ip, and port.
func (c *Client) StartExposeBdev(nqn, bdevName, nguid, ip, port string) error {
ip = spdkutil.NormalizeNvmeAddr(ip)

logrus.Infof("Exposing bdev with nqn %v, bdevName %v, nguid %v, ip %v, port %v", nqn, bdevName, nguid, ip, port)

nvmfTransportList, err := c.NvmfGetTransports("", "")
Expand All @@ -80,8 +104,10 @@ func (c *Client) StartExposeBdev(nqn, bdevName, nguid, ip, port string) error {
return err
}

logrus.Infof("Adding listener with transport address %v, transport service id %v, transport type %v, address family %v to subsystem with nqn %v", ip, port, spdktypes.NvmeTransportTypeTCP, spdktypes.NvmeAddressFamilyIPv4, nqn)
if _, err := c.NvmfSubsystemAddListener(nqn, ip, port, spdktypes.NvmeTransportTypeTCP, spdktypes.NvmeAddressFamilyIPv4); err != nil {
adrfam := DetectAddressFamily(ip)

logrus.Infof("Adding listener with transport address %v, transport service id %v, transport type %v, address family %v to subsystem with nqn %v", ip, port, spdktypes.NvmeTransportTypeTCP, adrfam, nqn)
if _, err := c.NvmfSubsystemAddListener(nqn, ip, port, spdktypes.NvmeTransportTypeTCP, adrfam); err != nil {
return err
Comment thread
COLDTURNIP marked this conversation as resolved.
}

Expand All @@ -95,6 +121,8 @@ func (c *Client) StartExposeBdev(nqn, bdevName, nguid, ip, port string) error {
// a unique controller-ID range per engine to avoid "Duplicate cntlid" errors
// when multiple targets share one subsystem NQN. Pass 0 for defaults.
func (c *Client) StartExposeBdevWithANAState(nqn, bdevName, nguid, nsUUID, ip, port string, anaState spdktypes.NvmfSubsystemListenerAnaState, minCntlid, maxCntlid uint16) error {
ip = spdkutil.NormalizeNvmeAddr(ip)

logrus.Infof("Exposing bdev with nqn %v, bdevName %v, nguid %v, nsUUID %v, ip %v, port %v, anaState %v, minCntlid %v, maxCntlid %v",
nqn, bdevName, nguid, nsUUID, ip, port, anaState, minCntlid, maxCntlid)

Expand All @@ -119,14 +147,16 @@ func (c *Client) StartExposeBdevWithANAState(nqn, bdevName, nguid, nsUUID, ip, p
return err
}

logrus.Infof("Adding listener with transport address %v, transport service id %v, transport type %v, address family %v to subsystem with nqn %v", ip, port, spdktypes.NvmeTransportTypeTCP, spdktypes.NvmeAddressFamilyIPv4, nqn)
if _, err := c.NvmfSubsystemAddListener(nqn, ip, port, spdktypes.NvmeTransportTypeTCP, spdktypes.NvmeAddressFamilyIPv4); err != nil {
adrfam := DetectAddressFamily(ip)

logrus.Infof("Adding listener with transport address %v, transport service id %v, transport type %v, address family %v to subsystem with nqn %v", ip, port, spdktypes.NvmeTransportTypeTCP, adrfam, nqn)
if _, err := c.NvmfSubsystemAddListener(nqn, ip, port, spdktypes.NvmeTransportTypeTCP, adrfam); err != nil {
return err
}

logrus.Infof("Setting listener ANA state to %v for subsystem with nqn %v", anaState, nqn)
if _, err := c.NvmfSubsystemListenerSetANAState(nqn, ip, port, spdktypes.NvmeTransportTypeTCP,
Comment thread
COLDTURNIP marked this conversation as resolved.
spdktypes.NvmeAddressFamilyIPv4, anaState, spdktypes.DefaultNvmfANAGroupID); err != nil {
adrfam, anaState, spdktypes.DefaultNvmfANAGroupID); err != nil {
return err
}

Expand Down
32 changes: 32 additions & 0 deletions pkg/spdk/client/advanced_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package client

import (
"testing"

spdktypes "github.com/longhorn/go-spdk-helper/pkg/spdk/types"
)

func TestDetectAddressFamily(t *testing.T) {
testCases := []struct {
name string
ip string
expected spdktypes.NvmeAddressFamily
}{
{"IPv4", "192.168.1.1", spdktypes.NvmeAddressFamilyIPv4},
{"IPv6", "fd00::1", spdktypes.NvmeAddressFamilyIPv6},
{"bracketed IPv6", "[fd00::1]", spdktypes.NvmeAddressFamilyIPv6},
{"IPv6 loopback", "::1", spdktypes.NvmeAddressFamilyIPv6},
{"empty", "", spdktypes.NvmeAddressFamilyIPv4},
{"malformed", "not-an-ip", spdktypes.NvmeAddressFamilyIPv4},
{"IPv4-mapped v6", "::ffff:10.0.0.1", spdktypes.NvmeAddressFamilyIPv4},
Comment thread
COLDTURNIP marked this conversation as resolved.
{"bracketed IPv4-mapped v6", "[::ffff:10.0.0.1]", spdktypes.NvmeAddressFamilyIPv4},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := DetectAddressFamily(tc.ip)
if got != tc.expected {
t.Errorf("DetectAddressFamily(%q) = %q, want %q", tc.ip, got, tc.expected)
}
})
}
}
11 changes: 11 additions & 0 deletions pkg/util/nvme.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"path/filepath"
"regexp"
"strings"
)

const (
Expand All @@ -29,3 +30,13 @@ func GetNvmeControllerNameFromNamespaceName(nsName string) string {
reg := regexp.MustCompile(`([^"]*)n\d+$`)
return reg.ReplaceAllString(nsName, "${1}")
}

// NormalizeNvmeAddr strips surrounding brackets from an IPv6 address if present.
// Both nvme-cli (-a flag) and SPDK expect bare IPv6 (e.g., "fd00::1" not "[fd00::1]").
func NormalizeNvmeAddr(ip string) string {
ip = strings.TrimSpace(ip)
if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") {
ip = ip[1 : len(ip)-1]
}
return ip
}
Comment thread
COLDTURNIP marked this conversation as resolved.
28 changes: 28 additions & 0 deletions pkg/util/nvme_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package util

import (
"testing"
)

func TestNormalizeNvmeAddr(t *testing.T) {
testCases := []struct {
name string
input string
expected string
}{
{"bare IPv6", "::1", "::1"},
{"bracketed IPv6", "[::1]", "::1"},
{"IPv4", "10.0.0.1", "10.0.0.1"},
{"bracketed fd00", "[fd00::1]", "fd00::1"},
{"empty", "", ""},
{"single bracket", "[", "["},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := NormalizeNvmeAddr(tc.input)
if got != tc.expected {
t.Errorf("NormalizeNvmeAddr(%q) = %q, want %q", tc.input, got, tc.expected)
}
})
}
}
Loading