Skip to content

Commit ba8d811

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 ba8d811

3 files changed

Lines changed: 231 additions & 37 deletions

File tree

internal/nvmeof/controller/controllerserver.go

Lines changed: 126 additions & 35 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,22 @@ 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 hostnames or an error if the YAML is invalid.
554+
func parseHostsParameters(params map[string]string) ([]string, error) {
555+
allowHostNQNs, exists := params[AllowHostNQNs]
556+
if !exists || allowHostNQNs == "" {
557+
return nil, nil
561558
}
562-
if err := parseParam(nvmeof.WMbytesPerSecond, nvmeof.WMbytesPerSecond, &qos.WMbytesPerSecond); err != nil {
563-
return nil, err
559+
var allowHostsList []string
560+
if err := yaml.Unmarshal([]byte(allowHostNQNs), &allowHostsList); err != nil {
561+
return nil, fmt.Errorf("invalid %s: must be a YAML list of strings: %w", AllowHostNQNs, err)
564562
}
565-
566-
if !hasAnyQoS {
563+
if len(allowHostsList) == 0 {
567564
return nil, nil
568565
}
569566

570-
return qos, nil
567+
return allowHostsList, nil
571568
}
572569

573570
// withGatewayConnection is a helper that manages the common pattern of:
@@ -628,6 +625,74 @@ func (cs *Server) withGatewayConnection(
628625
return fn(ctx, gateway, nvmeofData)
629626
}
630627

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

767833
networkMask := params["networkMask"]
768834
nvmeofData := &nvmeof.NVMeoFVolumeData{}
769835

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

@@ -781,7 +847,14 @@ func (cs *Server) createNVMeoFResources(
781847

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

856+
return nil, fmt.Errorf("failed to parse hosts parameters: %w", err)
857+
}
785858
// Step 2: Connect to gateway
786859
config, err := getGatewayConfigFromRequest(params)
787860
if err != nil {
@@ -829,7 +902,16 @@ func (cs *Server) createNVMeoFResources(
829902
return nvmeofData, fmt.Errorf("setting QoS limits failed: %w", err)
830903
}
831904
}
832-
905+
if hosts != nil {
906+
log.DebugLog(ctx, "Adding hosts to subsystem: %v", hosts)
907+
for _, host := range hosts {
908+
// TODO - for now we create host with empty DH-CHAP keys,
909+
// in the future we can extend the VAC parameters to allow passing DH-CHAP keys for each host if needed??
910+
if err := gateway.AddHost(ctx, nvmeofData.SubsystemNQN, host, nvmeof.DHCHAPKeys{}); err != nil {
911+
return nvmeofData, fmt.Errorf("adding host %s to subsystem failed: %w", host, err)
912+
}
913+
}
914+
}
833915
// Step 6: If using auto-listeners, query them back for storing in metadata
834916
if networkMask != "" {
835917
autoListeners, err := gateway.ListListeners(ctx, nvmeofData.SubsystemNQN)
@@ -1029,6 +1111,15 @@ func getHostNQNFromNodeID(nodeID string) (string, error) {
10291111
return prefix + nodeID, nil
10301112
}
10311113

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

internal/nvmeof/nvmeof.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,107 @@ func (gw *GatewayRpcClient) RemoveHost(ctx context.Context, subsystemNQN, hostNQ
605605
}
606606
}
607607

608+
// ListHosts lists all hosts in a subsystem.
609+
// Returns a slice of host NQNs that have access to the subsystem.
610+
func (gw *GatewayRpcClient) ListHosts(ctx context.Context, subsystemNQN string) ([]string, error) {
611+
log.DebugLog(ctx, "Listing hosts in subsystem %s", subsystemNQN)
612+
613+
req := &pb.ListHostsReq{
614+
Subsystem: subsystemNQN,
615+
}
616+
617+
resp, err := gw.client.ListHosts(ctx, req)
618+
if err != nil {
619+
return nil, fmt.Errorf("failed to list hosts in subsystem %s: %w", subsystemNQN, err)
620+
}
621+
if resp.GetStatus() != 0 {
622+
return nil, fmt.Errorf("gateway ListHosts returned error (status=%d): %s",
623+
resp.GetStatus(), resp.GetErrorMessage())
624+
}
625+
626+
// Extract host NQNs from response
627+
hosts := make([]string, 0, len(resp.GetHosts()))
628+
for _, host := range resp.GetHosts() {
629+
hosts = append(hosts, host.GetNqn())
630+
}
631+
632+
log.DebugLog(ctx, "Listed %d hosts in subsystem %s", len(hosts), subsystemNQN)
633+
634+
return hosts, nil
635+
}
636+
637+
// UpdateHostsForSubsystem reconciles the hosts in a subsystem to match the desired list.
638+
// It lists current hosts, then adds/removes hosts to ensure the subsystem has exactly
639+
// the hosts specified in desiredHosts.
640+
func (gw *GatewayRpcClient) UpdateHostsForSubsystem(
641+
ctx context.Context,
642+
subsystemNQN string,
643+
desiredHosts []string,
644+
) error {
645+
log.DebugLog(ctx, "Reconciling hosts for subsystem %s: desired=%v", subsystemNQN, desiredHosts)
646+
647+
// Get current hosts in the subsystem
648+
currentHosts, err := gw.ListHosts(ctx, subsystemNQN)
649+
if err != nil {
650+
return fmt.Errorf("failed to list current hosts: %w", err)
651+
}
652+
653+
// Convert to sets for easier comparison
654+
currentHostSet := make(map[string]bool)
655+
for _, host := range currentHosts {
656+
currentHostSet[host] = true
657+
}
658+
659+
desiredHostSet := make(map[string]bool)
660+
for _, host := range desiredHosts {
661+
desiredHostSet[host] = true
662+
}
663+
664+
// Determine hosts to add and remove
665+
var hostsToAdd []string
666+
var hostsToRemove []string
667+
668+
// Find hosts that need to be added
669+
for _, host := range desiredHosts {
670+
if !currentHostSet[host] {
671+
hostsToAdd = append(hostsToAdd, host)
672+
}
673+
}
674+
675+
// Find hosts that need to be removed
676+
for _, host := range currentHosts {
677+
if !desiredHostSet[host] {
678+
hostsToRemove = append(hostsToRemove, host)
679+
}
680+
}
681+
682+
log.DebugLog(ctx, "Host reconciliation for subsystem %s: current=%v, desired=%v, toAdd=%v, toRemove=%v",
683+
subsystemNQN, currentHosts, desiredHosts, hostsToAdd, hostsToRemove)
684+
685+
// Remove hosts that shouldn't be there
686+
for _, host := range hostsToRemove {
687+
log.DebugLog(ctx, "Removing host %s from subsystem %s", host, subsystemNQN)
688+
if err := gw.RemoveHost(ctx, subsystemNQN, host); err != nil {
689+
return fmt.Errorf("failed to remove host %s: %w", host, err)
690+
}
691+
}
692+
693+
// Add hosts that should be there
694+
for _, host := range hostsToAdd {
695+
log.DebugLog(ctx, "Adding host %s to subsystem %s", host, subsystemNQN)
696+
// Note: AddHost requires DHCHAPKeys, but for now we pass empty keys
697+
// TODO: Support DH-CHAP keys if needed for host updates
698+
if err := gw.AddHost(ctx, subsystemNQN, host, DHCHAPKeys{}); err != nil {
699+
return fmt.Errorf("failed to add host %s: %w", host, err)
700+
}
701+
}
702+
703+
log.DebugLog(ctx, "Successfully reconciled hosts for subsystem %s: added %d, removed %d",
704+
subsystemNQN, len(hostsToAdd), len(hostsToRemove))
705+
706+
return nil
707+
}
708+
608709
// List namespaces in a subsystem.
609710
func (gw *GatewayRpcClient) ListNamespaces(ctx context.Context, subsystemNQN string) (*pb.NamespacesInfo, error) {
610711
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 = fmt.Sprintf("nqn.2016-06.io.ceph:subsystem.%s", volumeID)
92+
}
9193
// set gw management info
9294
if nvmeofGatewayPortStr := parameters["nvmeofGatewayPort"]; nvmeofGatewayPortStr != "" {
9395
parsed, err := strconv.ParseUint(nvmeofGatewayPortStr, 10, 32)

0 commit comments

Comments
 (0)