Skip to content

Commit 74dd5eb

Browse files
COLDTURNIPderekbit
authored andcommitted
feat(network): ipv6 address family detection
Signed-off-by: Raphanus Lo <yunchang.lo@suse.com>
1 parent 49b12f8 commit 74dd5eb

6 files changed

Lines changed: 150 additions & 7 deletions

File tree

pkg/initiator/nvmecli.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
commonns "github.com/longhorn/go-common-libs/ns"
1111

1212
"github.com/longhorn/go-spdk-helper/pkg/types"
13+
14+
spdkutil "github.com/longhorn/go-spdk-helper/pkg/util"
1315
)
1416

1517
const (
@@ -234,9 +236,13 @@ func getHostID(executor *commonns.Executor) (string, error) {
234236
}
235237

236238
func discovery(hostID, hostNQN, ip, port string, executor *commonns.Executor) ([]DiscoveryPageEntry, error) {
239+
ip = spdkutil.NormalizeNvmeAddr(ip)
240+
237241
opts := []string{
238242
"discover",
239243
"-t", DefaultTransportType,
244+
// nvme-cli -a accepts bare IPv6 (no brackets). net.SplitHostPort callers
245+
// upstream strip brackets; util.NormalizeNvmeAddr is a safety net.
240246
"-a", ip,
241247
"-s", port,
242248
"-o", "json",
@@ -303,6 +309,8 @@ func discovery(hostID, hostNQN, ip, port string, executor *commonns.Executor) ([
303309
}
304310

305311
func connect(hostID, hostNQN, nqn, transpotType, ip, port string, executor *commonns.Executor) (string, error) {
312+
ip = spdkutil.NormalizeNvmeAddr(ip)
313+
306314
var err error
307315

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

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

@@ -412,7 +423,7 @@ func GetIPAndPortFromControllerAddress(address string) (string, string) {
412423
})
413424

414425
for _, part := range parts {
415-
keyVal := strings.Split(part, "=")
426+
keyVal := strings.SplitN(part, "=", 2)
416427
if len(keyVal) == 2 {
417428
key := strings.TrimSpace(keyVal[0])
418429
value := strings.TrimSpace(keyVal[1])

pkg/initiator/nvmecli_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package initiator
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestGetIPAndPortFromControllerAddressIPv6(t *testing.T) {
8+
testCases := []struct {
9+
name string
10+
address string
11+
expectedIP string
12+
expectedPort string
13+
}{
14+
{"IPv6 space-separated", "traddr=fd00::1 trsvcid=20001", "fd00::1", "20001"},
15+
{"IPv6 comma-separated", "traddr=fd00::1,trsvcid=20001", "fd00::1", "20001"},
16+
{"IPv6 loopback", "traddr=::1 trsvcid=4420", "::1", "4420"},
17+
{"IPv4 space-separated", "traddr=10.42.2.18 trsvcid=20006", "10.42.2.18", "20006"},
18+
{"IPv4 comma-separated", "traddr=10.42.2.18,trsvcid=20006", "10.42.2.18", "20006"},
19+
}
20+
for _, tc := range testCases {
21+
t.Run(tc.name, func(t *testing.T) {
22+
gotIP, gotPort := GetIPAndPortFromControllerAddress(tc.address)
23+
if gotIP != tc.expectedIP {
24+
t.Errorf("GetIPAndPortFromControllerAddress(%q) IP = %q, want %q", tc.address, gotIP, tc.expectedIP)
25+
}
26+
if gotPort != tc.expectedPort {
27+
t.Errorf("GetIPAndPortFromControllerAddress(%q) Port = %q, want %q", tc.address, gotPort, tc.expectedPort)
28+
}
29+
})
30+
}
31+
}

pkg/spdk/client/advanced.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package client
22

33
import (
4+
"net"
45
"path/filepath"
56

67
"github.com/sirupsen/logrus"
78

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

1011
spdktypes "github.com/longhorn/go-spdk-helper/pkg/spdk/types"
12+
spdkutil "github.com/longhorn/go-spdk-helper/pkg/util"
1113
)
1214

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

60+
// DetectAddressFamily returns the NVMe address family for the given IP.
61+
// Exported so downstream repos (longhorn-spdk-engine) can reuse it.
62+
func DetectAddressFamily(ip string) spdktypes.NvmeAddressFamily {
63+
if ip == "" {
64+
return spdktypes.NvmeAddressFamilyIPv4
65+
}
66+
67+
normalized := spdkutil.NormalizeNvmeAddr(ip)
68+
parsedIP := net.ParseIP(normalized)
69+
if parsedIP == nil {
70+
logrus.Warnf("Failed to parse IP %q, defaulting to IPv4", ip)
71+
return spdktypes.NvmeAddressFamilyIPv4
72+
}
73+
74+
if parsedIP.To4() == nil {
75+
return spdktypes.NvmeAddressFamilyIPv6
76+
}
77+
return spdktypes.NvmeAddressFamilyIPv4
78+
}
79+
5880
// StartExposeBdev exposes the bdev with the given nqn, bdevName, nguid, ip, and port.
5981
func (c *Client) StartExposeBdev(nqn, bdevName, nguid, ip, port string) error {
82+
ip = spdkutil.NormalizeNvmeAddr(ip)
83+
6084
logrus.Infof("Exposing bdev with nqn %v, bdevName %v, nguid %v, ip %v, port %v", nqn, bdevName, nguid, ip, port)
6185

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

83-
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)
84-
if _, err := c.NvmfSubsystemAddListener(nqn, ip, port, spdktypes.NvmeTransportTypeTCP, spdktypes.NvmeAddressFamilyIPv4); err != nil {
107+
adrfam := DetectAddressFamily(ip)
108+
109+
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)
110+
if _, err := c.NvmfSubsystemAddListener(nqn, ip, port, spdktypes.NvmeTransportTypeTCP, adrfam); err != nil {
85111
return err
86112
}
87113

@@ -95,6 +121,8 @@ func (c *Client) StartExposeBdev(nqn, bdevName, nguid, ip, port string) error {
95121
// a unique controller-ID range per engine to avoid "Duplicate cntlid" errors
96122
// when multiple targets share one subsystem NQN. Pass 0 for defaults.
97123
func (c *Client) StartExposeBdevWithANAState(nqn, bdevName, nguid, nsUUID, ip, port string, anaState spdktypes.NvmfSubsystemListenerAnaState, minCntlid, maxCntlid uint16) error {
124+
ip = spdkutil.NormalizeNvmeAddr(ip)
125+
98126
logrus.Infof("Exposing bdev with nqn %v, bdevName %v, nguid %v, nsUUID %v, ip %v, port %v, anaState %v, minCntlid %v, maxCntlid %v",
99127
nqn, bdevName, nguid, nsUUID, ip, port, anaState, minCntlid, maxCntlid)
100128

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

122-
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)
123-
if _, err := c.NvmfSubsystemAddListener(nqn, ip, port, spdktypes.NvmeTransportTypeTCP, spdktypes.NvmeAddressFamilyIPv4); err != nil {
150+
adrfam := DetectAddressFamily(ip)
151+
152+
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)
153+
if _, err := c.NvmfSubsystemAddListener(nqn, ip, port, spdktypes.NvmeTransportTypeTCP, adrfam); err != nil {
124154
return err
125155
}
126156

127157
logrus.Infof("Setting listener ANA state to %v for subsystem with nqn %v", anaState, nqn)
128158
if _, err := c.NvmfSubsystemListenerSetANAState(nqn, ip, port, spdktypes.NvmeTransportTypeTCP,
129-
spdktypes.NvmeAddressFamilyIPv4, anaState, spdktypes.DefaultNvmfANAGroupID); err != nil {
159+
adrfam, anaState, spdktypes.DefaultNvmfANAGroupID); err != nil {
130160
return err
131161
}
132162

pkg/spdk/client/advanced_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package client
2+
3+
import (
4+
"testing"
5+
6+
spdktypes "github.com/longhorn/go-spdk-helper/pkg/spdk/types"
7+
)
8+
9+
func TestDetectAddressFamily(t *testing.T) {
10+
testCases := []struct {
11+
name string
12+
ip string
13+
expected spdktypes.NvmeAddressFamily
14+
}{
15+
{"IPv4", "192.168.1.1", spdktypes.NvmeAddressFamilyIPv4},
16+
{"IPv6", "fd00::1", spdktypes.NvmeAddressFamilyIPv6},
17+
{"bracketed IPv6", "[fd00::1]", spdktypes.NvmeAddressFamilyIPv6},
18+
{"IPv6 loopback", "::1", spdktypes.NvmeAddressFamilyIPv6},
19+
{"empty", "", spdktypes.NvmeAddressFamilyIPv4},
20+
{"malformed", "not-an-ip", spdktypes.NvmeAddressFamilyIPv4},
21+
{"IPv4-mapped v6", "::ffff:10.0.0.1", spdktypes.NvmeAddressFamilyIPv4},
22+
{"bracketed IPv4-mapped v6", "[::ffff:10.0.0.1]", spdktypes.NvmeAddressFamilyIPv4},
23+
}
24+
for _, tc := range testCases {
25+
t.Run(tc.name, func(t *testing.T) {
26+
got := DetectAddressFamily(tc.ip)
27+
if got != tc.expected {
28+
t.Errorf("DetectAddressFamily(%q) = %q, want %q", tc.ip, got, tc.expected)
29+
}
30+
})
31+
}
32+
}

pkg/util/nvme.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"path/filepath"
66
"regexp"
7+
"strings"
78
)
89

910
const (
@@ -29,3 +30,13 @@ func GetNvmeControllerNameFromNamespaceName(nsName string) string {
2930
reg := regexp.MustCompile(`([^"]*)n\d+$`)
3031
return reg.ReplaceAllString(nsName, "${1}")
3132
}
33+
34+
// NormalizeNvmeAddr strips surrounding brackets from an IPv6 address if present.
35+
// Both nvme-cli (-a flag) and SPDK expect bare IPv6 (e.g., "fd00::1" not "[fd00::1]").
36+
func NormalizeNvmeAddr(ip string) string {
37+
ip = strings.TrimSpace(ip)
38+
if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") {
39+
ip = ip[1 : len(ip)-1]
40+
}
41+
return ip
42+
}

pkg/util/nvme_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package util
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestNormalizeNvmeAddr(t *testing.T) {
8+
testCases := []struct {
9+
name string
10+
input string
11+
expected string
12+
}{
13+
{"bare IPv6", "::1", "::1"},
14+
{"bracketed IPv6", "[::1]", "::1"},
15+
{"IPv4", "10.0.0.1", "10.0.0.1"},
16+
{"bracketed fd00", "[fd00::1]", "fd00::1"},
17+
{"empty", "", ""},
18+
{"single bracket", "[", "["},
19+
}
20+
for _, tc := range testCases {
21+
t.Run(tc.name, func(t *testing.T) {
22+
got := NormalizeNvmeAddr(tc.input)
23+
if got != tc.expected {
24+
t.Errorf("NormalizeNvmeAddr(%q) = %q, want %q", tc.input, got, tc.expected)
25+
}
26+
})
27+
}
28+
}

0 commit comments

Comments
 (0)