Skip to content

Commit 5d2664c

Browse files
Implement unique subsystem names for regular ONTAP NVMe driver
1 parent f0b7c8d commit 5d2664c

File tree

6 files changed

+144
-39
lines changed

6 files changed

+144
-39
lines changed

storage_drivers/ontap/api/abstraction_rest.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3430,6 +3430,7 @@ func (d OntapAPIREST) NVMeEnsureNamespaceMapped(ctx context.Context, subsystemUU
34303430
// NVMeEnsureNamespaceUnmapped first checks if a namespace is mapped to the subsystem and if it is mapped:
34313431
// a) removes the namespace from the subsystem
34323432
// b) deletes the subsystem if no more namespaces are attached to it
3433+
// c) Handles the case where multiple hosts are mapped to single common subsystem arising out of lengthy node names.
34333434
// If namespace is not mapped to subsystem, it is treated as success
34343435
// The function also returns a bool value along with error. A true value denotes the subsystem is deleted
34353436
// successfully and Published info can be removed for the NVMe volume
@@ -3459,8 +3460,15 @@ func (d OntapAPIREST) NVMeEnsureNamespaceUnmapped(ctx context.Context, hostNQN,
34593460
}
34603461
}
34613462

3462-
// In case of multiple hosts attached to a subsystem (e.g. in RWX case), do not delete the namespace,
3463-
// subsystem or the published info
3463+
// Below are the cases where multiple hosts are attached to a subsystem:
3464+
// case 1: RWX case - Multiple hosts are intentionally sharing the same namespace for ReadWriteMany access.
3465+
// In this scenario, do not delete the namespace mapping, as other hosts may still be using it.
3466+
// case 2: Common subsystem due to lengthy host names - When host names exceed the allowed length,
3467+
// multiple hosts may be mapped to a single "common" subsystem. In this scenario, the subsystem
3468+
// is not truly shared for RWX purposes, but is a result of host name truncation. Therefore,
3469+
// it is appropriate to remove the namespace mapping when detaching a host, as the mapping is
3470+
// not required for other hosts. This differs from the RWX case, where the namespace mapping
3471+
// must be preserved for legitimate multi-host access.
34643472
if len(subsystemHosts) > 1 {
34653473
if hostFound {
34663474
Logc(ctx).Infof("Multiple hosts are attached to this subsystem %v. Do not delete namespace or subsystem",
@@ -3470,6 +3478,21 @@ func (d OntapAPIREST) NVMeEnsureNamespaceUnmapped(ctx context.Context, hostNQN,
34703478
return false, err
34713479
}
34723480
}
3481+
3482+
// Check if there are multiple namespaces attached to the subsystem,
3483+
// This indicates case 2, so, remove the namespace only.
3484+
count, err := d.api.NVMeNamespaceCount(ctx, subsystemUUID)
3485+
if err != nil {
3486+
return false, fmt.Errorf("error getting namespace count for subsystem %s; %v", subsystemUUID, err)
3487+
}
3488+
3489+
if count > 1 {
3490+
err = d.api.NVMeSubsystemRemoveNamespace(ctx, subsystemUUID, namespaceUUID)
3491+
if err != nil {
3492+
return false, fmt.Errorf("error removing namespace %s from subsystem %s; %v", namespaceUUID, subsystemUUID, err)
3493+
}
3494+
}
3495+
34733496
return false, nil
34743497
}
34753498

@@ -3493,7 +3516,7 @@ func (d OntapAPIREST) NVMeEnsureNamespaceUnmapped(ctx context.Context, hostNQN,
34933516
return false, fmt.Errorf("error getting namespace count for subsystem %s; %v", subsystemUUID, err)
34943517
}
34953518

3496-
// Delete the subsystem if no. of namespaces is 0
3519+
// Delete the subsystem if the namespace count for this subsystem is 0 after removal.
34973520
if count == 0 {
34983521
if err := d.api.NVMeSubsystemDelete(ctx, subsystemUUID); err != nil {
34993522
return false, fmt.Errorf("error deleting subsystem %s; %v", subsystemUUID, err)

storage_drivers/ontap/api/abstraction_rest_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,7 @@ func TestNVMeNamespaceUnmapped(t *testing.T) {
928928
mock.EXPECT().NVMeIsNamespaceMapped(ctx, subsystemUUID, nsUUID).Return(true, nil).Times(1)
929929
mock.EXPECT().NVMeGetHostsOfSubsystem(ctx, subsystemUUID).Return([]*models.NvmeSubsystemHost{host1, host2}, nil).Times(1)
930930
mock.EXPECT().NVMeRemoveHostFromSubsystem(ctx, hostNQN, subsystemUUID).Return(nil).Times(1)
931+
mock.EXPECT().NVMeNamespaceCount(ctx, subsystemUUID).Return(int64(1), nil).Times(1)
931932

932933
removePublishInfo, err = oapi.NVMeEnsureNamespaceUnmapped(ctx, hostNQN, subsystemUUID, nsUUID)
933934

@@ -999,11 +1000,84 @@ func TestNVMeNamespaceUnmapped(t *testing.T) {
9991000
mock.EXPECT().NVMeIsNamespaceMapped(ctx, subsystemUUID, nsUUID).Return(true, nil).Times(1)
10001001
mock.EXPECT().NVMeGetHostsOfSubsystem(ctx, subsystemUUID).Return([]*models.NvmeSubsystemHost{host1, host2},
10011002
nil).Times(1)
1003+
mock.EXPECT().NVMeNamespaceCount(ctx, subsystemUUID).Return(int64(1), nil).Times(1)
10021004

10031005
removePublishInfo, err = oapi.NVMeEnsureNamespaceUnmapped(ctx, nonExistentHostNQN, subsystemUUID, nsUUID)
10041006

10051007
assert.Equal(t, false, removePublishInfo, "nqn is unmapped")
10061008
assert.NoError(t, err)
1009+
1010+
// case 13: Multiple hosts with error getting namespace count
1011+
mock.EXPECT().ClientConfig().Return(clientConfig).AnyTimes()
1012+
mock.EXPECT().NVMeIsNamespaceMapped(ctx, subsystemUUID, nsUUID).Return(true, nil).Times(1)
1013+
mock.EXPECT().NVMeGetHostsOfSubsystem(ctx, subsystemUUID).Return([]*models.NvmeSubsystemHost{host1, host2}, nil).Times(1)
1014+
mock.EXPECT().NVMeRemoveHostFromSubsystem(ctx, hostNQN, subsystemUUID).Return(nil).Times(1)
1015+
mock.EXPECT().NVMeNamespaceCount(ctx, subsystemUUID).Return(int64(0), errors.New("Error getting namespace count")).Times(1)
1016+
1017+
removePublishInfo, err = oapi.NVMeEnsureNamespaceUnmapped(ctx, hostNQN, subsystemUUID, nsUUID)
1018+
1019+
assert.Equal(t, false, removePublishInfo, "subsystem removed")
1020+
assert.Error(t, err)
1021+
1022+
// case 14: Multiple hosts with namespace count > 1, error removing namespace
1023+
mock.EXPECT().ClientConfig().Return(clientConfig).AnyTimes()
1024+
mock.EXPECT().NVMeIsNamespaceMapped(ctx, subsystemUUID, nsUUID).Return(true, nil).Times(1)
1025+
mock.EXPECT().NVMeGetHostsOfSubsystem(ctx, subsystemUUID).Return([]*models.NvmeSubsystemHost{host1, host2}, nil).Times(1)
1026+
mock.EXPECT().NVMeRemoveHostFromSubsystem(ctx, hostNQN, subsystemUUID).Return(nil).Times(1)
1027+
mock.EXPECT().NVMeNamespaceCount(ctx, subsystemUUID).Return(int64(2), nil).Times(1)
1028+
mock.EXPECT().NVMeSubsystemRemoveNamespace(ctx, subsystemUUID, nsUUID).Return(errors.New("Error removing namespace")).Times(1)
1029+
1030+
removePublishInfo, err = oapi.NVMeEnsureNamespaceUnmapped(ctx, hostNQN, subsystemUUID, nsUUID)
1031+
1032+
assert.Equal(t, false, removePublishInfo, "subsystem removed")
1033+
assert.Error(t, err)
1034+
1035+
// case 15: Multiple hosts with namespace count > 1, success removing namespace
1036+
mock.EXPECT().ClientConfig().Return(clientConfig).AnyTimes()
1037+
mock.EXPECT().NVMeIsNamespaceMapped(ctx, subsystemUUID, nsUUID).Return(true, nil).Times(1)
1038+
mock.EXPECT().NVMeGetHostsOfSubsystem(ctx, subsystemUUID).Return([]*models.NvmeSubsystemHost{host1, host2}, nil).Times(1)
1039+
mock.EXPECT().NVMeRemoveHostFromSubsystem(ctx, hostNQN, subsystemUUID).Return(nil).Times(1)
1040+
mock.EXPECT().NVMeNamespaceCount(ctx, subsystemUUID).Return(int64(3), nil).Times(1)
1041+
mock.EXPECT().NVMeSubsystemRemoveNamespace(ctx, subsystemUUID, nsUUID).Return(nil).Times(1)
1042+
1043+
removePublishInfo, err = oapi.NVMeEnsureNamespaceUnmapped(ctx, hostNQN, subsystemUUID, nsUUID)
1044+
1045+
assert.Equal(t, false, removePublishInfo, "subsystem is not removed due to multiple namespaces")
1046+
assert.NoError(t, err)
1047+
1048+
// case 16: Multiple hosts with namespace count = 1 (RWX case), no removal of namespace or subsystem
1049+
mock.EXPECT().ClientConfig().Return(clientConfig).AnyTimes()
1050+
mock.EXPECT().NVMeIsNamespaceMapped(ctx, subsystemUUID, nsUUID).Return(true, nil).Times(1)
1051+
mock.EXPECT().NVMeGetHostsOfSubsystem(ctx, subsystemUUID).Return([]*models.NvmeSubsystemHost{host1, host2}, nil).Times(1)
1052+
mock.EXPECT().NVMeRemoveHostFromSubsystem(ctx, hostNQN, subsystemUUID).Return(nil).Times(1)
1053+
mock.EXPECT().NVMeNamespaceCount(ctx, subsystemUUID).Return(int64(1), nil).Times(1)
1054+
1055+
removePublishInfo, err = oapi.NVMeEnsureNamespaceUnmapped(ctx, hostNQN, subsystemUUID, nsUUID)
1056+
1057+
assert.Equal(t, false, removePublishInfo, "subsystem is not removed in RWX case")
1058+
assert.NoError(t, err)
1059+
1060+
// case 17: Single host but different from requested host (nonExistentHostNQN), subsystem and namespace not removed
1061+
mock.EXPECT().ClientConfig().Return(clientConfig).AnyTimes()
1062+
mock.EXPECT().NVMeIsNamespaceMapped(ctx, subsystemUUID, nsUUID).Return(true, nil).Times(1)
1063+
mock.EXPECT().NVMeGetHostsOfSubsystem(ctx, subsystemUUID).Return([]*models.NvmeSubsystemHost{host1}, nil).Times(1)
1064+
1065+
removePublishInfo, err = oapi.NVMeEnsureNamespaceUnmapped(ctx, nonExistentHostNQN, subsystemUUID, nsUUID)
1066+
1067+
assert.Equal(t, false, removePublishInfo, "subsystem is not removed when host doesn't match")
1068+
assert.NoError(t, err)
1069+
1070+
// case 18: Namespace count > 0 after removal, subsystem not deleted
1071+
mock.EXPECT().ClientConfig().Return(clientConfig).AnyTimes()
1072+
mock.EXPECT().NVMeIsNamespaceMapped(ctx, subsystemUUID, nsUUID).Return(true, nil).Times(1)
1073+
mock.EXPECT().NVMeGetHostsOfSubsystem(ctx, subsystemUUID).Return([]*models.NvmeSubsystemHost{host1}, nil).Times(1)
1074+
mock.EXPECT().NVMeSubsystemRemoveNamespace(ctx, subsystemUUID, nsUUID).Return(nil).Times(1)
1075+
mock.EXPECT().NVMeNamespaceCount(ctx, subsystemUUID).Return(int64(2), nil).Times(1)
1076+
1077+
removePublishInfo, err = oapi.NVMeEnsureNamespaceUnmapped(ctx, hostNQN, subsystemUUID, nsUUID)
1078+
1079+
assert.Equal(t, true, removePublishInfo, "subsystem has remaining namespaces")
1080+
assert.NoError(t, err)
10071081
}
10081082

10091083
func TestNVMeIsNamespaceMapped(t *testing.T) {

storage_drivers/ontap/ontap_common.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5085,6 +5085,8 @@ func getUniqueNodeSpecificSubsystemName(
50855085

50865086
// Construct the subsystem name
50875087
completeSSName := fmt.Sprintf("%s_%s_%s", prefix, nodeName, tridentUUID)
5088+
// Skip any underscores at the beginning
5089+
completeSSName = strings.TrimLeft(completeSSName, "_")
50885090
finalSSName := completeSSName
50895091

50905092
// Ensure the final name does not exceed the maximum length
@@ -5098,6 +5100,7 @@ func getUniqueNodeSpecificSubsystemName(
50985100

50995101
base64Str := base64.StdEncoding.EncodeToString(u[:])
51005102
finalSSName = fmt.Sprintf("%s_%s_%s", prefix, nodeName, base64Str)
5103+
finalSSName = strings.TrimLeft(finalSSName, "_")
51015104

51025105
if len(finalSSName) > maxSubsystemLength {
51035106
// If even after Trident UUID reduction, the length is more than max length,

storage_drivers/ontap/ontap_common_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9729,6 +9729,26 @@ func TestGetUniqueNodeSpecificSubsystemName(t *testing.T) {
97299729
expectedFinal: "",
97309730
expectError: true,
97319731
},
9732+
{
9733+
description: "Valid input, with no prefix passed",
9734+
nodeName: "node1",
9735+
tridentUUID: tridentUUID,
9736+
prefix: "",
9737+
maxSubsystemLength: 64,
9738+
expectedBestCase: fmt.Sprintf("node1_%s", tridentUUID),
9739+
expectedFinal: fmt.Sprintf("node1_%s", tridentUUID),
9740+
expectError: false,
9741+
},
9742+
{
9743+
description: "Even hash is more than limit, truncation needed with no prefix",
9744+
nodeName: "averylongnodenameexceedingthelimit",
9745+
tridentUUID: tridentUUID,
9746+
prefix: "",
9747+
maxSubsystemLength: 32,
9748+
expectedBestCase: fmt.Sprintf("averylongnodenameexceedingthelimit_%s", tridentUUID),
9749+
expectedFinal: fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("averylongnodenameexceedingthelimit_%s", tridentUUID))))[:32],
9750+
expectError: false,
9751+
},
97329752
}
97339753

97349754
for _, test := range tests {

storage_drivers/ontap/ontap_san_nvme.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const (
5858
defaultNamespaceBlockSize = 4096
5959
// maximumSubsystemNameLength represent the max length of subsystem name
6060
maximumSubsystemNameLength = 64
61+
// nvmeSubsystemPrefix Subsystem prefix for ONTAP NVMe driver (empty for legacy compatibility)
62+
nvmeSubsystemPrefix = ""
6163
)
6264

6365
// Namespace attributes stored in its comment field. These fields are useful for docker context.
@@ -912,14 +914,26 @@ func (d *NVMeStorageDriver) Publish(
912914
// When FS type is RAW, we create a new subsystem per namespace,
913915
// else we use the subsystem created for that particular node
914916
var ssName string
917+
var completeSSName string
915918
if volConfig.FileSystem == filesystem.Raw {
916919
ssName = getNamespaceSpecificSubsystemName(name, pvName)
917920
} else {
918-
ssName = getNodeSpecificSubsystemName(publishInfo.HostName, publishInfo.TridentUUID)
921+
if completeSSName, ssName, err = getUniqueNodeSpecificSubsystemName(
922+
publishInfo.HostName, publishInfo.TridentUUID, nvmeSubsystemPrefix, maximumSubsystemNameLength); err != nil {
923+
return fmt.Errorf("failed to create node specific subsystem name: %w", err)
924+
}
925+
}
926+
927+
// Update the subsystem comment
928+
var ssComment string
929+
if completeSSName == "" {
930+
ssComment = ssName
931+
} else {
932+
ssComment = completeSSName
919933
}
920934

921935
// Checks if subsystem exists and creates a new one if not
922-
subsystem, err := d.API.NVMeSubsystemCreate(ctx, ssName, ssName)
936+
subsystem, err := d.API.NVMeSubsystemCreate(ctx, ssName, ssComment)
923937
if err != nil {
924938
Logc(ctx).Errorf("subsystem create failed, %v", err)
925939
return err
@@ -1705,16 +1719,6 @@ func (d *NVMeStorageDriver) ParseNVMeNamespaceCommentString(_ context.Context, c
17051719
return nil, fmt.Errorf("nsAttrs field not found in Namespace comment")
17061720
}
17071721

1708-
func getNodeSpecificSubsystemName(nodeName, tridentUUID string) string {
1709-
subsystemName := fmt.Sprintf("%s-%s", nodeName, tridentUUID)
1710-
if len(subsystemName) > maximumSubsystemNameLength {
1711-
// If the new subsystem name is over the subsystem character limit, it means the host name is too long.
1712-
subsystemPrefixLength := maximumSubsystemNameLength - len(tridentUUID) - 1
1713-
subsystemName = fmt.Sprintf("%s-%s", nodeName[:subsystemPrefixLength], tridentUUID)
1714-
}
1715-
return subsystemName
1716-
}
1717-
17181722
// getNamespaceSpecificSubsystemName constructs the subsystem name using the name passed.
17191723
func getNamespaceSpecificSubsystemName(name, pvName string) string {
17201724
subSystemPrefix := "s"

storage_drivers/ontap/ontap_san_nvme_test.go

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,25 +1510,6 @@ func TestGetNamespaceSpecificSubsystemName(t *testing.T) {
15101510
assert.Equal(t, got_name, expected_name)
15111511
}
15121512

1513-
func TestGetNodeSpecificSubsystemName(t *testing.T) {
1514-
// case 1: subsystem, name is shorter than 64 char
1515-
nodeName := "fakeNodeName"
1516-
tridentUUID := "fakeUUID"
1517-
expected := "fakeNodeName-fakeUUID"
1518-
1519-
got := getNodeSpecificSubsystemName(nodeName, tridentUUID)
1520-
1521-
assert.Equal(t, got, expected)
1522-
1523-
// case 2: subsystem name is longer than 64 char
1524-
nodeName = "fakeNodeNamefakeNodeNamefakeNodeNamefakeNodeNamefakeNodeNamefakeNodeNamefakeNodeNamefakeNodeName"
1525-
expected = "fakeNodeNamefakeNodeNamefakeNodeNamefakeNodeNamefakeNod-fakeUUID"
1526-
1527-
got = getNodeSpecificSubsystemName(nodeName, tridentUUID)
1528-
1529-
assert.Equal(t, got, expected)
1530-
}
1531-
15321513
func TestPublish(t *testing.T) {
15331514
mockCtrl := gomock.NewController(t)
15341515
mock := mockapi.NewMockOntapAPI(mockCtrl)
@@ -1599,7 +1580,7 @@ func TestPublish(t *testing.T) {
15991580
publishInfo.HostNQN = "fakeHostNQN"
16001581
mock.EXPECT().VolumeInfo(ctx, volConfig.InternalName).Return(flexVol, nil).Times(1)
16011582
mock.EXPECT().NVMeNamespaceGetByName(ctx, gomock.Any()).Return(namespace, nil).Times(1)
1602-
mock.EXPECT().NVMeSubsystemCreate(ctx, "fakeHostName-fakeUUID", "fakeHostName-fakeUUID").Return(subsystem, fmt.Errorf("Error creating subsystem")).Times(1)
1583+
mock.EXPECT().NVMeSubsystemCreate(ctx, "fakeHostName_fakeUUID", "fakeHostName_fakeUUID").Return(subsystem, fmt.Errorf("Error creating subsystem")).Times(1)
16031584

16041585
err = d.Publish(ctx, volConfig, publishInfo)
16051586

@@ -1608,7 +1589,7 @@ func TestPublish(t *testing.T) {
16081589
// case 6: Error creating subsystem in CSI Context
16091590
d.Config.DriverContext = tridentconfig.ContextCSI
16101591
mock.EXPECT().VolumeInfo(ctx, volConfig.InternalName).Return(flexVol, nil).Times(1)
1611-
mock.EXPECT().NVMeSubsystemCreate(ctx, "fakeHostName-fakeUUID", "fakeHostName-fakeUUID").Return(subsystem, fmt.Errorf("Error creating subsystem")).Times(1)
1592+
mock.EXPECT().NVMeSubsystemCreate(ctx, "fakeHostName_fakeUUID", "fakeHostName_fakeUUID").Return(subsystem, fmt.Errorf("Error creating subsystem")).Times(1)
16121593

16131594
err = d.Publish(ctx, volConfig, publishInfo)
16141595

@@ -1626,7 +1607,7 @@ func TestPublish(t *testing.T) {
16261607
// case 8: Error while adding host nqn to subsystem
16271608
publishInfo.HostNQN = "fakeHostNQN"
16281609
mock.EXPECT().VolumeInfo(ctx, volConfig.InternalName).Return(flexVol, nil).Times(1)
1629-
mock.EXPECT().NVMeSubsystemCreate(ctx, "fakeHostName-fakeUUID", "fakeHostName-fakeUUID").Return(subsystem, nil).Times(1)
1610+
mock.EXPECT().NVMeSubsystemCreate(ctx, "fakeHostName_fakeUUID", "fakeHostName_fakeUUID").Return(subsystem, nil).Times(1)
16301611
mock.EXPECT().NVMeAddHostToSubsystem(ctx, publishInfo.HostNQN, subsystem.UUID).Return(fmt.Errorf("Error adding host nqnq to subsystem")).Times(1)
16311612

16321613
err = d.Publish(ctx, volConfig, publishInfo)
@@ -1635,7 +1616,7 @@ func TestPublish(t *testing.T) {
16351616

16361617
// case 9: Error returned by NVMeEnsureNamespaceMapped
16371618
mock.EXPECT().VolumeInfo(ctx, volConfig.InternalName).Return(flexVol, nil).Times(1)
1638-
mock.EXPECT().NVMeSubsystemCreate(ctx, "fakeHostName-fakeUUID", "fakeHostName-fakeUUID").Return(subsystem, nil).Times(1)
1619+
mock.EXPECT().NVMeSubsystemCreate(ctx, "fakeHostName_fakeUUID", "fakeHostName_fakeUUID").Return(subsystem, nil).Times(1)
16391620
mock.EXPECT().NVMeAddHostToSubsystem(ctx, publishInfo.HostNQN, subsystem.UUID).Return(nil).Times(1)
16401621
mock.EXPECT().NVMeEnsureNamespaceMapped(ctx, gomock.Any(), gomock.Any()).Return(fmt.Errorf("Error returned by NVMeEnsureNamespaceMapped")).Times(1)
16411622

@@ -1645,7 +1626,7 @@ func TestPublish(t *testing.T) {
16451626

16461627
// case 10: Success
16471628
mock.EXPECT().VolumeInfo(ctx, volConfig.InternalName).Return(flexVol, nil).Times(1)
1648-
mock.EXPECT().NVMeSubsystemCreate(ctx, "fakeHostName-fakeUUID", "fakeHostName-fakeUUID").Return(subsystem, nil).Times(1)
1629+
mock.EXPECT().NVMeSubsystemCreate(ctx, "fakeHostName_fakeUUID", "fakeHostName_fakeUUID").Return(subsystem, nil).Times(1)
16491630
mock.EXPECT().NVMeAddHostToSubsystem(ctx, publishInfo.HostNQN, subsystem.UUID).Return(nil).Times(1)
16501631
mock.EXPECT().NVMeEnsureNamespaceMapped(ctx, gomock.Any(), gomock.Any()).Return(nil).Times(1)
16511632

0 commit comments

Comments
 (0)