Skip to content

Commit d9bc798

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 64b7ce9 commit d9bc798

4 files changed

Lines changed: 224 additions & 39 deletions

File tree

internal/nvmeof/controller/controllerserver.go

Lines changed: 130 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"strconv"
2626

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

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

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

@@ -352,11 +353,22 @@ func (cs *Server) ControllerModifyVolume(
352353

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

361373
return &csi.ControllerModifyVolumeResponse{}, nil
362374
}
@@ -466,6 +478,11 @@ func validateCreateVolumeRequest(req *csi.CreateVolumeRequest) error {
466478
if err != nil {
467479
return fmt.Errorf("invalid NVMe-oF QoS parameters: %w", err)
468480
}
481+
482+
_, err = parseHostsParameters(mutableParams)
483+
if err != nil {
484+
return fmt.Errorf("invalid NVMe-oF hosts parameters (for external clients): %w", err)
485+
}
469486
err = validateDHCHAPParameter(params["dhchapMode"])
470487
if err != nil {
471488
return err
@@ -532,42 +549,24 @@ func validateNetworkMask(networkMask string) error {
532549
return nil
533550
}
534551

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

570-
return qos, nil
569+
return allowHostsList, nil
571570
}
572571

573572
// withGatewayConnection is a helper that manages the common pattern of:
@@ -628,6 +627,74 @@ func (cs *Server) withGatewayConnection(
628627
return fn(ctx, gateway, nvmeofData)
629628
}
630629

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

767835
networkMask := params["networkMask"]
768836
nvmeofData := &nvmeof.NVMeoFVolumeData{}
769837

770-
if err := nvmeofData.SetFromParameters(params); err != nil {
838+
if err := nvmeofData.SetFromParameters(params, volumeID); err != nil {
771839
return nil, fmt.Errorf("failed to set NVMe-oF volume data: %w", err)
772840
}
773841

@@ -781,7 +849,15 @@ func (cs *Server) createNVMeoFResources(
781849

782850
return nil, fmt.Errorf("failed to parse QoS parameters: %w", err)
783851
}
852+
// If VAC with hosts list is given (for external client)
853+
// We need to parse the hosts list and pass it to the gateway for creating host entries
854+
// and adding them to the subsystem.
855+
hosts, err := parseHostsParameters(mutableParams)
856+
if err != nil {
857+
log.ErrorLog(ctx, "failed to parse NVMe-oF hosts parameters: %v", err)
784858

859+
return nil, fmt.Errorf("failed to parse hosts parameters: %w", err)
860+
}
785861
// Step 2: Connect to gateway
786862
config, err := getGatewayConfigFromRequest(params)
787863
if err != nil {
@@ -829,7 +905,16 @@ func (cs *Server) createNVMeoFResources(
829905
return nvmeofData, fmt.Errorf("setting QoS limits failed: %w", err)
830906
}
831907
}
832-
908+
if hosts != nil {
909+
log.DebugLog(ctx, "Adding hosts to subsystem: %v", hosts)
910+
for _, host := range hosts {
911+
// TODO - for now we create host with empty DH-CHAP keys,
912+
// in the future we can extend the VAC parameters to allow passing DH-CHAP keys for each host if needed??
913+
if err := gateway.AddHost(ctx, nvmeofData.SubsystemNQN, host, nvmeof.DHCHAPKeys{}); err != nil {
914+
return nvmeofData, fmt.Errorf("adding host %s to subsystem failed: %w", host, err)
915+
}
916+
}
917+
}
833918
// Step 6: If using auto-listeners, query them back for storing in metadata
834919
if networkMask != "" {
835920
autoListeners, err := gateway.ListListeners(ctx, nvmeofData.SubsystemNQN)
@@ -1029,6 +1114,15 @@ func getHostNQNFromNodeID(nodeID string) (string, error) {
10291114
return prefix + nodeID, nil
10301115
}
10311116

1117+
// AllowHostNQNs is the VolumeAttributesClass mutable parameter key for specifying
1118+
// a YAML list of host NQNs to allow access to a volume. Use "*" to allow any host.
1119+
// Example:
1120+
//
1121+
// allowHostNQNs: |
1122+
// - nqn.2014-08.org.nvmexpress:host1
1123+
// - nqn.2014-08.org.nvmexpress:host2
1124+
const AllowHostNQNs = "allowHostNQNs"
1125+
10321126
// VolumeContext metadata keys.
10331127
const (
10341128
// NVMe-oF resource info.

internal/nvmeof/nvmeof.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"crypto/rand"
2222
"fmt"
2323
"math/big"
24+
"slices"
2425
"syscall"
2526

2627
pb "github.com/ceph/ceph-nvmeof/lib/go/nvmeof"
@@ -605,6 +606,74 @@ func (gw *GatewayRpcClient) RemoveHost(ctx context.Context, subsystemNQN, hostNQ
605606
}
606607
}
607608

609+
// ListHosts lists all hosts in a subsystem.
610+
// Returns a slice of host NQNs that have access to the subsystem.
611+
func (gw *GatewayRpcClient) ListHosts(ctx context.Context, subsystemNQN string) ([]string, error) {
612+
log.DebugLog(ctx, "Listing hosts in subsystem %s", subsystemNQN)
613+
614+
req := &pb.ListHostsReq{
615+
Subsystem: subsystemNQN,
616+
}
617+
618+
resp, err := gw.client.ListHosts(ctx, req)
619+
if err != nil {
620+
return nil, fmt.Errorf("failed to list hosts in subsystem %s: %w", subsystemNQN, err)
621+
}
622+
if resp.GetStatus() != 0 {
623+
return nil, fmt.Errorf("gateway ListHosts returned error (status=%d): %s",
624+
resp.GetStatus(), resp.GetErrorMessage())
625+
}
626+
627+
// Extract host NQNs from response
628+
hosts := make([]string, 0, len(resp.GetHosts()))
629+
for _, host := range resp.GetHosts() {
630+
hosts = append(hosts, host.GetNqn())
631+
}
632+
633+
log.DebugLog(ctx, "Listed %d hosts in subsystem %s", len(hosts), subsystemNQN)
634+
635+
return hosts, nil
636+
}
637+
638+
// UpdateHostsForSubsystem reconciles the hosts in a subsystem to match the desired list.
639+
// It lists current hosts, then adds/removes hosts to ensure the subsystem has exactly
640+
// the hosts specified in desiredHosts.
641+
func (gw *GatewayRpcClient) UpdateHostsForSubsystem(
642+
ctx context.Context,
643+
subsystemNQN string,
644+
desiredHosts []string,
645+
) error {
646+
currentHosts, err := gw.ListHosts(ctx, subsystemNQN)
647+
if err != nil {
648+
return fmt.Errorf("failed to list current hosts: %w", err)
649+
}
650+
651+
log.DebugLog(ctx, "Host reconciliation for subsystem %s: current=%v, desired=%v",
652+
subsystemNQN, currentHosts, desiredHosts)
653+
654+
for _, host := range currentHosts {
655+
if !slices.Contains(desiredHosts, host) {
656+
log.DebugLog(ctx, "Removing host %s from subsystem %s", host, subsystemNQN)
657+
if err := gw.RemoveHost(ctx, subsystemNQN, host); err != nil {
658+
return fmt.Errorf("failed to remove host %s: %w", host, err)
659+
}
660+
}
661+
}
662+
663+
for _, host := range desiredHosts {
664+
if !slices.Contains(currentHosts, host) {
665+
log.DebugLog(ctx, "Adding host %s to subsystem %s", host, subsystemNQN)
666+
// Note: AddHost requires DHCHAPKeys, but for now we pass empty keys
667+
// TODO: Support DH-CHAP keys if needed for host updates
668+
if err := gw.AddHost(ctx, subsystemNQN, host, DHCHAPKeys{}); err != nil {
669+
return fmt.Errorf("failed to add host %s: %w", host, err)
670+
}
671+
}
672+
}
673+
674+
return nil
675+
}
676+
608677
// List namespaces in a subsystem.
609678
func (gw *GatewayRpcClient) ListNamespaces(ctx context.Context, subsystemNQN string) (*pb.NamespacesInfo, error) {
610679
log.DebugLog(ctx, "Listing namespaces in subsystem %s", subsystemNQN)

internal/nvmeof/volume.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,12 @@ func SetupListeners(listenersJSON string) ([]ListenerDetails, error) {
8484
// It extracts the subsystem NQN, gateway management info, security config, and
8585
// listener info from the parameters.
8686
// It also applies default values to listeners if necessary.
87-
func (v *NVMeoFVolumeData) SetFromParameters(parameters map[string]string) error {
87+
func (v *NVMeoFVolumeData) SetFromParameters(parameters map[string]string, volumeID string) error {
8888
// set subsystem NQN
8989
v.SubsystemNQN = parameters["subsystemNQN"]
90-
90+
if v.SubsystemNQN == "" {
91+
v.SubsystemNQN = "nqn.2016-06.io.ceph:subsystem." + volumeID
92+
}
9193
// set gw management info
9294
if nvmeofGatewayPortStr := parameters["nvmeofGatewayPort"]; nvmeofGatewayPortStr != "" {
9395
parsed, err := strconv.ParseUint(nvmeofGatewayPortStr, 10, 32)

internal/nvmeof/volume_test.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,30 @@ func TestSetFromParameters(t *testing.T) {
206206
expected: NVMeoFVolumeData{},
207207
expectError: true,
208208
},
209+
{
210+
name: "missing subsystem NQN (should use default)",
211+
params: map[string]string{
212+
"nvmeofGatewayAddress": "10.241.1.9",
213+
"nvmeofGatewayPort": "5500",
214+
"listeners": `[{"hostname": "nvmeof-gw-a", "port": 4420, "address": "10.92.3.12"}]`,
215+
},
216+
expected: NVMeoFVolumeData{
217+
SubsystemNQN: "nqn.2016-06.io.ceph:subsystem.test-volume-id",
218+
GatewayManagementInfo: GatewayConfig{
219+
Address: "10.241.1.9",
220+
Port: 5500,
221+
},
222+
ListenerInfo: []ListenerDetails{
223+
{Hostname: "nvmeof-gw-a", GatewayAddress: GatewayAddress{Port: 4420, Address: "10.92.3.12"}},
224+
},
225+
},
226+
expectError: false,
227+
},
209228
}
210229
for _, test := range tests {
211230
vol := &NVMeoFVolumeData{}
212-
err := vol.SetFromParameters(test.params)
231+
volID := "test-volume-id"
232+
err := vol.SetFromParameters(test.params, volID)
213233
if test.expectError {
214234
require.Error(t, err)
215235
} else {

0 commit comments

Comments
 (0)