Skip to content

Commit ad3285f

Browse files
committed
feat(ipv6): add IPv6 address family detection and nvme-cli IPv6 fixes
- Export DetectAddressFamily() in pkg/spdk/client for adrfam auto-detection - Replace hardcoded NvmeAddressFamilyIPv4 in StartExposeBdev/StartExposeBdevWithANAState - Add normalizeNvmeAddr() in pkg/initiator to strip brackets from IPv6 for nvme-cli -a - Use SplitN in GetIPAndPortFromControllerAddress to correctly split IPv6 traddr values Address review comments (PR #276): - DetectAddressFamily: strip brackets before net.ParseIP so [fd00::1] is handled correctly - normalizeNvmeAddr: downgrade log from Warn to Debug (bracketed IPv6 is normal input) - GetIPAndPortFromControllerAddress: add IPv6 example to function comment Signed-off-by: Raphanus Lo <yunchang.lo@suse.com>
1 parent 49b12f8 commit ad3285f

4 files changed

Lines changed: 135 additions & 7 deletions

File tree

pkg/initiator/nvmecli.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,24 @@ import (
77
"strconv"
88
"strings"
99

10+
"github.com/sirupsen/logrus"
11+
1012
commonns "github.com/longhorn/go-common-libs/ns"
1113

1214
"github.com/longhorn/go-spdk-helper/pkg/types"
1315
)
1416

17+
// normalizeNvmeAddr strips brackets from an IPv6 address if present.
18+
// nvme-cli -a flag expects bare IPv6 (e.g., "fd00::1" not "[fd00::1]").
19+
func normalizeNvmeAddr(ip string) string {
20+
if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
21+
stripped := ip[1 : len(ip)-1]
22+
logrus.Debugf("Normalizing bracketed NVMe address: %q -> %q", ip, stripped)
23+
return stripped
24+
}
25+
return ip
26+
}
27+
1528
const (
1629
nvmeBinary = "nvme"
1730

@@ -234,9 +247,13 @@ func getHostID(executor *commonns.Executor) (string, error) {
234247
}
235248

236249
func discovery(hostID, hostNQN, ip, port string, executor *commonns.Executor) ([]DiscoveryPageEntry, error) {
250+
ip = normalizeNvmeAddr(ip)
251+
237252
opts := []string{
238253
"discover",
239254
"-t", DefaultTransportType,
255+
// nvme-cli -a accepts bare IPv6 (no brackets). net.SplitHostPort callers
256+
// upstream strip brackets; normalizeNvmeAddr is a safety net.
240257
"-a", ip,
241258
"-s", port,
242259
"-o", "json",
@@ -303,6 +320,8 @@ func discovery(hostID, hostNQN, ip, port string, executor *commonns.Executor) ([
303320
}
304321

305322
func connect(hostID, hostNQN, nqn, transpotType, ip, port string, executor *commonns.Executor) (string, error) {
323+
ip = normalizeNvmeAddr(ip)
324+
306325
var err error
307326

308327
opts := []string{
@@ -322,6 +341,8 @@ func connect(hostID, hostNQN, nqn, transpotType, ip, port string, executor *comm
322341
opts = append(opts, "-q", hostNQN)
323342
}
324343
if ip != "" {
344+
// nvme-cli -a accepts bare IPv6 (no brackets). net.SplitHostPort callers
345+
// upstream strip brackets; normalizeNvmeAddr is a safety net.
325346
opts = append(opts, "-a", ip)
326347
}
327348
if port != "" {
@@ -402,8 +423,9 @@ func extractJSONString(str string) (string, error) {
402423
return "", fmt.Errorf("invalid JSON string")
403424
}
404425

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

@@ -412,7 +434,7 @@ func GetIPAndPortFromControllerAddress(address string) (string, string) {
412434
})
413435

414436
for _, part := range parts {
415-
keyVal := strings.Split(part, "=")
437+
keyVal := strings.SplitN(part, "=", 2)
416438
if len(keyVal) == 2 {
417439
key := strings.TrimSpace(keyVal[0])
418440
value := strings.TrimSpace(keyVal[1])

pkg/initiator/nvmecli_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package initiator
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+
}
29+
30+
func TestGetIPAndPortFromControllerAddressIPv6(t *testing.T) {
31+
testCases := []struct {
32+
name string
33+
address string
34+
expectedIP string
35+
expectedPort string
36+
}{
37+
{"IPv6 space-separated", "traddr=fd00::1 trsvcid=20001", "fd00::1", "20001"},
38+
{"IPv6 comma-separated", "traddr=fd00::1,trsvcid=20001", "fd00::1", "20001"},
39+
{"IPv6 loopback", "traddr=::1 trsvcid=4420", "::1", "4420"},
40+
{"IPv4 space-separated", "traddr=10.42.2.18 trsvcid=20006", "10.42.2.18", "20006"},
41+
{"IPv4 comma-separated", "traddr=10.42.2.18,trsvcid=20006", "10.42.2.18", "20006"},
42+
}
43+
for _, tc := range testCases {
44+
t.Run(tc.name, func(t *testing.T) {
45+
gotIP, gotPort := GetIPAndPortFromControllerAddress(tc.address)
46+
if gotIP != tc.expectedIP {
47+
t.Errorf("GetIPAndPortFromControllerAddress(%q) IP = %q, want %q", tc.address, gotIP, tc.expectedIP)
48+
}
49+
if gotPort != tc.expectedPort {
50+
t.Errorf("GetIPAndPortFromControllerAddress(%q) Port = %q, want %q", tc.address, gotPort, tc.expectedPort)
51+
}
52+
})
53+
}
54+
}

pkg/spdk/client/advanced.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package client
22

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

67
"github.com/sirupsen/logrus"
@@ -55,6 +56,23 @@ func (c *Client) DeleteDevice(bdevAioName, lvsName string) (err error) {
5556
return nil
5657
}
5758

59+
// DetectAddressFamily returns the NVMe address family for the given IP.
60+
// Exported so downstream repos (longhorn-spdk-engine) can reuse it.
61+
func DetectAddressFamily(ip string) spdktypes.NvmeAddressFamily {
62+
// Strip brackets so bracketed IPv6 forms like "[fd00::1]" parse correctly.
63+
if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
64+
ip = ip[1 : len(ip)-1]
65+
}
66+
if parsedIP := net.ParseIP(ip); parsedIP != nil {
67+
if parsedIP.To4() == nil {
68+
return spdktypes.NvmeAddressFamilyIPv6
69+
}
70+
} else if ip != "" {
71+
logrus.Warnf("Failed to parse IP %q for address family detection, defaulting to IPv4", ip)
72+
}
73+
return spdktypes.NvmeAddressFamilyIPv4
74+
}
75+
5876
// StartExposeBdev exposes the bdev with the given nqn, bdevName, nguid, ip, and port.
5977
func (c *Client) StartExposeBdev(nqn, bdevName, nguid, ip, port string) error {
6078
logrus.Infof("Exposing bdev with nqn %v, bdevName %v, nguid %v, ip %v, port %v", nqn, bdevName, nguid, ip, port)
@@ -80,8 +98,10 @@ func (c *Client) StartExposeBdev(nqn, bdevName, nguid, ip, port string) error {
8098
return err
8199
}
82100

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 {
101+
adrfam := DetectAddressFamily(ip)
102+
103+
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)
104+
if _, err := c.NvmfSubsystemAddListener(nqn, ip, port, spdktypes.NvmeTransportTypeTCP, adrfam); err != nil {
85105
return err
86106
}
87107

@@ -119,14 +139,16 @@ func (c *Client) StartExposeBdevWithANAState(nqn, bdevName, nguid, nsUUID, ip, p
119139
return err
120140
}
121141

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 {
142+
adrfam := DetectAddressFamily(ip)
143+
144+
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)
145+
if _, err := c.NvmfSubsystemAddListener(nqn, ip, port, spdktypes.NvmeTransportTypeTCP, adrfam); err != nil {
124146
return err
125147
}
126148

127149
logrus.Infof("Setting listener ANA state to %v for subsystem with nqn %v", anaState, nqn)
128150
if _, err := c.NvmfSubsystemListenerSetANAState(nqn, ip, port, spdktypes.NvmeTransportTypeTCP,
129-
spdktypes.NvmeAddressFamilyIPv4, anaState, spdktypes.DefaultNvmfANAGroupID); err != nil {
151+
adrfam, anaState, spdktypes.DefaultNvmfANAGroupID); err != nil {
130152
return err
131153
}
132154

pkg/spdk/client/advanced_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
{"IPv6 loopback", "::1", spdktypes.NvmeAddressFamilyIPv6},
18+
{"empty", "", spdktypes.NvmeAddressFamilyIPv4},
19+
{"malformed", "not-an-ip", spdktypes.NvmeAddressFamilyIPv4},
20+
{"IPv4-mapped v6", "::ffff:10.0.0.1", spdktypes.NvmeAddressFamilyIPv4},
21+
}
22+
for _, tc := range testCases {
23+
t.Run(tc.name, func(t *testing.T) {
24+
got := DetectAddressFamily(tc.ip)
25+
if got != tc.expected {
26+
t.Errorf("DetectAddressFamily(%q) = %q, want %q", tc.ip, got, tc.expected)
27+
}
28+
})
29+
}
30+
}

0 commit comments

Comments
 (0)