Skip to content

Commit 852fd6c

Browse files
committed
nvmeof: add external client support via allowHostNQNs parameter
Implements dynamic host access control for NVMe-oF volumes to support external(non-Kubernetes) clients through VolumeAttributesClass. Changes: - Add `parseHostsParameters()` to parse YAML host list from VAC - Implement `modifyNVMeoFHosts()` for runtime host updates - Add `ListHosts()` and `UpdateHostsForSubsystem()` for host updateing - Support allowHostNQNs parameter in CreateVolume and ControllerModifyVolume - Auto-generate subsystem NQN from volumeID if not provided Users can now specify external hosts in VAC mutable parameters: allowHostNQNs: | - nqn.2014-08.org.nvmexpress:host1 - nqn.2014-08.org.nvmexpress:host2 Signed-off-by: gadi-didi <gadi.didi@ibm.com>
1 parent b4aeae9 commit 852fd6c

5 files changed

Lines changed: 328 additions & 46 deletions

File tree

internal/nvmeof/controller/controllerserver.go

Lines changed: 128 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"strconv"
2727

2828
"github.com/container-storage-interface/spec/lib/go/csi"
29+
"github.com/ghodss/yaml"
2930
"google.golang.org/grpc/codes"
3031
"google.golang.org/grpc/status"
3132

@@ -167,7 +168,7 @@ func (cs *Server) CreateVolume(
167168
}
168169
}()
169170

170-
nvmeofData, err = cs.createNVMeoFResources(ctx, req, rbdPoolName, rbdRadosNameSpace, rbdImageName)
171+
nvmeofData, err = cs.createNVMeoFResources(ctx, req, rbdPoolName, rbdRadosNameSpace, rbdImageName, volumeID)
171172
if err != nil {
172173
log.ErrorLog(ctx, "NVMe-oF resource setup failed for volumeID %s: %v", volumeID, err)
173174

@@ -353,11 +354,22 @@ func (cs *Server) ControllerModifyVolume(
353354

354355
return nil, status.Errorf(codes.InvalidArgument, "failed to parse QoS parameters: %v", err)
355356
}
357+
hostsList, err := parseHostsParameters(params)
358+
if err != nil {
359+
log.ErrorLog(ctx, "failed to parse NVMe-oF hosts parameters: %v", err)
360+
361+
return nil, status.Errorf(codes.InvalidArgument, "failed to parse hosts parameters: %v", err)
362+
}
356363
if nvmeofQoS != nil {
357364
if err := cs.modifyNVMeoFQoS(ctx, req, nvmeofQoS); err != nil {
358365
return nil, err
359366
}
360367
}
368+
if hostsList != nil {
369+
if err := cs.modifyNVMeoFHosts(ctx, req, hostsList); err != nil {
370+
return nil, err
371+
}
372+
}
361373

362374
return &csi.ControllerModifyVolumeResponse{}, nil
363375
}
@@ -425,14 +437,10 @@ func validateDHCHAPParameter(dhchapMode string) error {
425437
func validateCreateVolumeRequest(req *csi.CreateVolumeRequest) error {
426438
// Validate required parameters
427439
params := req.GetParameters()
428-
requiredParams := []string{
429-
"subsystemNQN", "nvmeofGatewayAddress",
430-
}
431-
for _, param := range requiredParams {
432-
if params[param] == "" {
433-
return fmt.Errorf("missing required parameter: %s", param)
434-
}
440+
if params["nvmeofGatewayAddress"] == "" {
441+
return errors.New("missing required parameter nvmeofGatewayAddress")
435442
}
443+
436444
// Validate listeners JSON if provided
437445
countOfListeners, err := validateListenersParameter(params["listeners"])
438446
if err != nil {
@@ -467,6 +475,11 @@ func validateCreateVolumeRequest(req *csi.CreateVolumeRequest) error {
467475
if err != nil {
468476
return fmt.Errorf("invalid NVMe-oF QoS parameters: %w", err)
469477
}
478+
479+
_, err = parseHostsParameters(mutableParams)
480+
if err != nil {
481+
return fmt.Errorf("invalid NVMe-oF hosts parameters (for external clients): %w", err)
482+
}
470483
err = validateDHCHAPParameter(params["dhchapMode"])
471484
if err != nil {
472485
return err
@@ -533,42 +546,24 @@ func validateNetworkMask(networkMask string) error {
533546
return nil
534547
}
535548

536-
// parseQoSParameters extracts and parses QoS parameters from the given map.
537-
func parseQoSParameters(params map[string]string) (*nvmeof.NVMeoFQosVolume, error) {
538-
qos := &nvmeof.NVMeoFQosVolume{}
539-
hasAnyQoS := false
540-
541-
parseParam := func(key, name string, dest **uint64) error {
542-
if val, exists := params[key]; exists && val != "" {
543-
parsed, err := strconv.ParseUint(val, 10, 64)
544-
if err != nil {
545-
return fmt.Errorf("invalid %s: %w", name, err)
546-
}
547-
*dest = &parsed
548-
hasAnyQoS = true
549-
}
550-
551-
return nil
552-
}
553-
554-
if err := parseParam(nvmeof.RwIosPerSecond, nvmeof.RwIosPerSecond, &qos.RwIosPerSecond); err != nil {
555-
return nil, err
556-
}
557-
if err := parseParam(nvmeof.RwMbytesPerSecond, nvmeof.RwMbytesPerSecond, &qos.RwMbytesPerSecond); err != nil {
558-
return nil, err
559-
}
560-
if err := parseParam(nvmeof.RMbytesPerSecond, nvmeof.RMbytesPerSecond, &qos.RMbytesPerSecond); err != nil {
561-
return nil, err
549+
// parseHostsParameters parses the hosts yaml list parameter and validates its contents.
550+
// It returns a slice of hostNQNs or an error if the YAML is invalid.
551+
// allowHostNQNs entry can be empty, in that case it means no hosts are allowed to access
552+
// the subsystem (empty allow list).
553+
func parseHostsParameters(params map[string]string) ([]string, error) {
554+
allowHostNQNs, exists := params[AllowHostNQNs]
555+
if !exists {
556+
return nil, nil
562557
}
563-
if err := parseParam(nvmeof.WMbytesPerSecond, nvmeof.WMbytesPerSecond, &qos.WMbytesPerSecond); err != nil {
564-
return nil, err
558+
var allowHostsList []string
559+
if allowHostNQNs == "" {
560+
return allowHostsList, nil
565561
}
566-
567-
if !hasAnyQoS {
568-
return nil, nil
562+
if err := yaml.Unmarshal([]byte(allowHostNQNs), &allowHostsList); err != nil {
563+
return nil, fmt.Errorf("invalid %s: must be a YAML list of strings: %w", AllowHostNQNs, err)
569564
}
570565

571-
return qos, nil
566+
return allowHostsList, nil
572567
}
573568

574569
// withGatewayConnection is a helper that manages the common pattern of:
@@ -629,6 +624,69 @@ func (cs *Server) withGatewayConnection(
629624
return fn(ctx, gateway, nvmeofData)
630625
}
631626

627+
// modifyNVMeoFHosts handles adding or removing hosts from the subsystem based on the provided list of host NQNs.
628+
func (cs *Server) modifyNVMeoFHosts(ctx context.Context, req *csi.ControllerModifyVolumeRequest, hosts []string) error {
629+
volumeID := req.GetVolumeId()
630+
631+
return cs.withGatewayConnection(ctx, req, volumeID, func(
632+
ctx context.Context,
633+
gateway *nvmeof.GatewayRpcClient,
634+
nvmeofData *nvmeof.NVMeoFVolumeData,
635+
) error {
636+
log.DebugLog(ctx, "Modifying hosts for subsystem=%s, nsid=%d: desired hosts=%v",
637+
nvmeofData.SubsystemNQN, nvmeofData.NamespaceID, hosts)
638+
639+
err := gateway.UpdateHostsForSubsystem(ctx, nvmeofData.SubsystemNQN, hosts)
640+
if err != nil {
641+
log.ErrorLog(ctx, "Failed to update hosts for subsystem: %v", err)
642+
643+
return status.Errorf(codes.Internal, "failed to update hosts for subsystem: %v", err)
644+
}
645+
646+
log.DebugLog(ctx, "Successfully modified hosts for volume %s", volumeID)
647+
648+
return nil
649+
})
650+
}
651+
652+
// parseQoSParameters extracts and parses QoS parameters from the given map.
653+
func parseQoSParameters(params map[string]string) (*nvmeof.NVMeoFQosVolume, error) {
654+
qos := &nvmeof.NVMeoFQosVolume{}
655+
hasAnyQoS := false
656+
657+
parseParam := func(key, name string, dest **uint64) error {
658+
if val, exists := params[key]; exists && val != "" {
659+
parsed, err := strconv.ParseUint(val, 10, 64)
660+
if err != nil {
661+
return fmt.Errorf("invalid %s: %w", name, err)
662+
}
663+
*dest = &parsed
664+
hasAnyQoS = true
665+
}
666+
667+
return nil
668+
}
669+
670+
if err := parseParam(nvmeof.RwIosPerSecond, nvmeof.RwIosPerSecond, &qos.RwIosPerSecond); err != nil {
671+
return nil, err
672+
}
673+
if err := parseParam(nvmeof.RwMbytesPerSecond, nvmeof.RwMbytesPerSecond, &qos.RwMbytesPerSecond); err != nil {
674+
return nil, err
675+
}
676+
if err := parseParam(nvmeof.RMbytesPerSecond, nvmeof.RMbytesPerSecond, &qos.RMbytesPerSecond); err != nil {
677+
return nil, err
678+
}
679+
if err := parseParam(nvmeof.WMbytesPerSecond, nvmeof.WMbytesPerSecond, &qos.WMbytesPerSecond); err != nil {
680+
return nil, err
681+
}
682+
683+
if !hasAnyQoS {
684+
return nil, nil
685+
}
686+
687+
return qos, nil
688+
}
689+
632690
// modifyNVMeoFQoS handles NVMe-oF gateway QoS modification.
633691
func (cs *Server) modifyNVMeoFQoS(
634692
ctx context.Context,
@@ -760,15 +818,16 @@ func (cs *Server) createNVMeoFResources(
760818
req *csi.CreateVolumeRequest,
761819
rbdPoolName,
762820
rbdRadosNameSpace,
763-
rbdImageName string,
821+
rbdImageName,
822+
volumeID string,
764823
) (*nvmeof.NVMeoFVolumeData, error) {
765824
// Step 1: Extract parameters (already validated)
766825
params := req.GetParameters()
767826

768827
networkMask := params["networkMask"]
769828
nvmeofData := &nvmeof.NVMeoFVolumeData{}
770829

771-
if err := nvmeofData.SetFromParameters(params); err != nil {
830+
if err := nvmeofData.SetFromParameters(params, volumeID); err != nil {
772831
return nil, fmt.Errorf("failed to set NVMe-oF volume data: %w", err)
773832
}
774833

@@ -782,7 +841,15 @@ func (cs *Server) createNVMeoFResources(
782841

783842
return nil, fmt.Errorf("failed to parse QoS parameters: %w", err)
784843
}
844+
// If VAC with hosts list is given (for external client)
845+
// We need to parse the hosts list and pass it to the gateway for creating host entries
846+
// and adding them to the subsystem.
847+
hosts, err := parseHostsParameters(mutableParams)
848+
if err != nil {
849+
log.ErrorLog(ctx, "failed to parse NVMe-oF hosts parameters: %v", err)
785850

851+
return nil, fmt.Errorf("failed to parse hosts parameters: %w", err)
852+
}
786853
// Step 2: Connect to gateway
787854
config, err := getGatewayConfigFromRequest(params)
788855
if err != nil {
@@ -830,7 +897,16 @@ func (cs *Server) createNVMeoFResources(
830897
return nvmeofData, fmt.Errorf("setting QoS limits failed: %w", err)
831898
}
832899
}
833-
900+
if hosts != nil {
901+
log.DebugLog(ctx, "Adding hosts to subsystem: %v", hosts)
902+
for _, host := range hosts {
903+
// TODO - for now we create host with empty DH-CHAP keys,
904+
// in the future we can extend the VAC parameters to allow passing DH-CHAP keys for each host if needed??
905+
if err := gateway.AddHost(ctx, nvmeofData.SubsystemNQN, host, nvmeof.DHCHAPKeys{}); err != nil {
906+
return nvmeofData, fmt.Errorf("adding host %s to subsystem failed: %w", host, err)
907+
}
908+
}
909+
}
834910
// Step 6: If using auto-listeners, query them back for storing in metadata
835911
if networkMask != "" {
836912
autoListeners, err := gateway.ListListeners(ctx, nvmeofData.SubsystemNQN)
@@ -1030,6 +1106,15 @@ func getHostNQNFromNodeID(nodeID string) (string, error) {
10301106
return prefix + nodeID, nil
10311107
}
10321108

1109+
// AllowHostNQNs is the VolumeAttributesClass mutable parameter key for specifying
1110+
// a YAML list of host NQNs to allow access to a volume. Use "*" to allow any host.
1111+
// Example:
1112+
//
1113+
// allowHostNQNs: |
1114+
// - nqn.2014-08.org.nvmexpress:host1
1115+
// - nqn.2014-08.org.nvmexpress:host2
1116+
const AllowHostNQNs = "allowHostNQNs"
1117+
10331118
// VolumeContext metadata keys.
10341119
const (
10351120
// NVMe-oF resource info.

internal/nvmeof/controller/qos_test.go renamed to internal/nvmeof/controller/mutable_params_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,109 @@ func TestParseQoSParameters(t *testing.T) {
151151
})
152152
}
153153
}
154+
155+
func TestParseHostParameters(t *testing.T) {
156+
t.Parallel()
157+
158+
tests := []struct {
159+
name string
160+
params map[string]string
161+
expected []string
162+
expectError bool
163+
}{
164+
{
165+
name: "no allowHostNQNs parameter",
166+
params: map[string]string{},
167+
expected: nil,
168+
},
169+
{
170+
name: "empty allowHostNQNs (deny all hosts)",
171+
params: map[string]string{
172+
AllowHostNQNs: "",
173+
},
174+
expected: nil, // Empty string returns nil slice, not empty slice
175+
},
176+
{
177+
name: "single host NQN",
178+
params: map[string]string{
179+
AllowHostNQNs: `- nqn.2014-08.org.nvmexpress:host1`,
180+
},
181+
expected: []string{"nqn.2014-08.org.nvmexpress:host1"},
182+
},
183+
{
184+
name: "multiple host NQNs",
185+
params: map[string]string{
186+
AllowHostNQNs: `- nqn.2014-08.org.nvmexpress:host1
187+
- nqn.2014-08.org.nvmexpress:host2
188+
- nqn.2014-08.org.nvmexpress:host3`,
189+
},
190+
expected: []string{
191+
"nqn.2014-08.org.nvmexpress:host1",
192+
"nqn.2014-08.org.nvmexpress:host2",
193+
"nqn.2014-08.org.nvmexpress:host3",
194+
},
195+
},
196+
{
197+
name: "wildcard to allow any host",
198+
params: map[string]string{
199+
AllowHostNQNs: `- "*"`,
200+
},
201+
expected: []string{"*"},
202+
},
203+
{
204+
name: "YAML list in flow style",
205+
params: map[string]string{
206+
AllowHostNQNs: `["nqn.2014-08.org.nvmexpress:host1", "nqn.2014-08.org.nvmexpress:host2"]`,
207+
},
208+
expected: []string{
209+
"nqn.2014-08.org.nvmexpress:host1",
210+
"nqn.2014-08.org.nvmexpress:host2",
211+
},
212+
},
213+
{
214+
name: "invalid YAML - not a list (plain string)",
215+
params: map[string]string{
216+
AllowHostNQNs: `nqn.2014-08.org.nvmexpress:host1`,
217+
},
218+
expectError: true,
219+
},
220+
{
221+
name: "invalid YAML - map instead of list",
222+
params: map[string]string{
223+
AllowHostNQNs: `host1: nqn.2014-08.org.nvmexpress:host1`,
224+
},
225+
expectError: true,
226+
},
227+
{
228+
name: "invalid YAML - unclosed quote",
229+
params: map[string]string{
230+
AllowHostNQNs: `- "nqn.2014-08.org.nvmexpress:host1`,
231+
},
232+
expectError: true,
233+
},
234+
{
235+
name: "other parameters present but no allowHostNQNs",
236+
params: map[string]string{
237+
"someOtherParam": "value",
238+
"nvmeofRWIOsPerSecond": "10000",
239+
},
240+
expected: nil,
241+
},
242+
}
243+
244+
for _, tt := range tests {
245+
t.Run(tt.name, func(t *testing.T) {
246+
t.Parallel()
247+
248+
result, err := parseHostsParameters(tt.params)
249+
250+
if tt.expectError {
251+
require.Error(t, err)
252+
assert.Nil(t, result)
253+
} else {
254+
require.NoError(t, err)
255+
assert.Equal(t, tt.expected, result)
256+
}
257+
})
258+
}
259+
}

0 commit comments

Comments
 (0)