mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-24 17:04:30 +00:00
NVMe/TCP transport support in the CSI driver so Kubernetes pods can mount block volumes via NVMe alongside (or instead of) iSCSI. Transport selection: NVMe preferred when nvme_tcp module loaded + metadata present + nvmeUtil available. Fail-fast on NVMe errors (no silent iSCSI fallback). .transport file persists across CSI restarts. Key changes: - BuildNQN() single source of truth for NQN construction (naming.go) - NVMeUtil interface + realNVMeUtil wrapping nvme-cli (nvme_util.go) - NodeStageVolume/Unstage/Expand dual-transport paths (node.go) - NvmeAddr/NQN fields in VolumeInfo, Controller contexts - VolumeManager NvmeAddr()/VolumeNQN() getters - BlockService NvmeListenAddr()/NQN() accessors - 27 unit tests + 26 QA adversarial tests (nvme_node_test.go, qa_cp102) - Fix: flaky TestQA_Node_ConcurrentStageUnstage (pre-alloc temp dirs) Review fixes applied: F1 (NQN format mismatch), F2 (CreateVolume drops NVMe context), F3 (IsConnected error classification), F4 (findSubsys path validation), F5 (MasterVolumeClient NVMe gap documented). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
176 lines
4.4 KiB
Go
176 lines
4.4 KiB
Go
package csi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/container-storage-interface/spec/lib/go/csi"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
// DriverConfig holds configuration for the CSI driver.
|
|
type DriverConfig struct {
|
|
Endpoint string // CSI endpoint (unix:///csi/csi.sock)
|
|
DataDir string // volume data directory
|
|
ISCSIAddr string // local iSCSI target listen address
|
|
IQNPrefix string // IQN prefix for volumes
|
|
NVMeAddr string // local NVMe/TCP target listen address (empty = NVMe disabled)
|
|
NQNPrefix string // NQN prefix for NVMe subsystems
|
|
NodeID string // node identifier
|
|
Logger *log.Logger
|
|
|
|
// Control-plane mode fields.
|
|
MasterAddr string // master address for control-plane mode (empty = local/standalone)
|
|
Mode string // "controller", "node", "all" (default "all")
|
|
}
|
|
|
|
// CSIDriver manages the gRPC server and CSI services.
|
|
type CSIDriver struct {
|
|
identity *identityServer
|
|
controller *controllerServer
|
|
node *nodeServer
|
|
mgr *VolumeManager
|
|
server *grpc.Server
|
|
endpoint string
|
|
logger *log.Logger
|
|
}
|
|
|
|
// NewCSIDriver creates a new CSI driver from the given configuration.
|
|
func NewCSIDriver(cfg DriverConfig) (*CSIDriver, error) {
|
|
if cfg.NodeID == "" {
|
|
return nil, fmt.Errorf("csi: node ID is required")
|
|
}
|
|
if cfg.Logger == nil {
|
|
cfg.Logger = log.Default()
|
|
}
|
|
if cfg.Mode == "" {
|
|
cfg.Mode = "all"
|
|
}
|
|
switch cfg.Mode {
|
|
case "controller", "node", "all":
|
|
// valid
|
|
default:
|
|
return nil, fmt.Errorf("csi: invalid mode %q, must be controller/node/all", cfg.Mode)
|
|
}
|
|
|
|
d := &CSIDriver{
|
|
identity: &identityServer{},
|
|
endpoint: cfg.Endpoint,
|
|
logger: cfg.Logger,
|
|
}
|
|
|
|
// Create VolumeManager for modes that need local volume management.
|
|
var mgr *VolumeManager
|
|
needsLocalMgr := cfg.Mode == "all" && cfg.MasterAddr == "" || cfg.Mode == "node"
|
|
if needsLocalMgr {
|
|
mgr = NewVolumeManager(cfg.DataDir, cfg.ISCSIAddr, cfg.IQNPrefix, cfg.Logger,
|
|
VolumeManagerOpts{NvmeAddr: cfg.NVMeAddr, NQNPrefix: cfg.NQNPrefix})
|
|
d.mgr = mgr
|
|
}
|
|
|
|
// Create backend for controller.
|
|
var backend VolumeBackend
|
|
if cfg.Mode == "controller" || cfg.Mode == "all" {
|
|
if cfg.MasterAddr != "" {
|
|
backend = NewMasterVolumeClient(cfg.MasterAddr, nil)
|
|
} else if mgr != nil {
|
|
backend = NewLocalVolumeBackend(mgr)
|
|
} else {
|
|
return nil, fmt.Errorf("csi: controller mode requires either --master or --data-dir")
|
|
}
|
|
d.controller = &controllerServer{backend: backend}
|
|
}
|
|
|
|
// Create node server.
|
|
if cfg.Mode == "node" || cfg.Mode == "all" {
|
|
d.node = &nodeServer{
|
|
mgr: mgr, // may be nil in controller-only mode
|
|
nodeID: cfg.NodeID,
|
|
iqnPrefix: cfg.IQNPrefix,
|
|
nqnPrefix: cfg.NQNPrefix,
|
|
iscsiUtil: &realISCSIUtil{},
|
|
nvmeUtil: &realNVMeUtil{},
|
|
mountUtil: &realMountUtil{},
|
|
logger: cfg.Logger,
|
|
staged: make(map[string]*stagedVolumeInfo),
|
|
}
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
// Run starts the volume manager and gRPC server. Blocks until Stop is called.
|
|
func (d *CSIDriver) Run() error {
|
|
if d.mgr != nil {
|
|
if err := d.mgr.Start(context.Background()); err != nil {
|
|
return fmt.Errorf("csi: start volume manager: %w", err)
|
|
}
|
|
}
|
|
|
|
// Parse endpoint URL.
|
|
proto, addr, err := parseEndpoint(d.endpoint)
|
|
if err != nil {
|
|
return fmt.Errorf("csi: parse endpoint: %w", err)
|
|
}
|
|
|
|
// Remove existing socket file if present.
|
|
if proto == "unix" {
|
|
os.Remove(addr)
|
|
}
|
|
|
|
ln, err := net.Listen(proto, addr)
|
|
if err != nil {
|
|
return fmt.Errorf("csi: listen %s: %w", d.endpoint, err)
|
|
}
|
|
|
|
d.server = grpc.NewServer()
|
|
csi.RegisterIdentityServer(d.server, d.identity)
|
|
if d.controller != nil {
|
|
csi.RegisterControllerServer(d.server, d.controller)
|
|
}
|
|
if d.node != nil {
|
|
csi.RegisterNodeServer(d.server, d.node)
|
|
}
|
|
|
|
d.logger.Printf("CSI driver serving on %s", d.endpoint)
|
|
return d.server.Serve(ln)
|
|
}
|
|
|
|
// Stop gracefully shuts down the gRPC server and volume manager.
|
|
func (d *CSIDriver) Stop() {
|
|
if d.server != nil {
|
|
d.server.GracefulStop()
|
|
}
|
|
if d.mgr != nil {
|
|
d.mgr.Stop()
|
|
}
|
|
}
|
|
|
|
// parseEndpoint parses a CSI endpoint string (unix:///path or tcp://host:port).
|
|
func parseEndpoint(ep string) (string, string, error) {
|
|
if strings.HasPrefix(ep, "unix://") {
|
|
u, err := url.Parse(ep)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
addr := u.Path
|
|
if u.Host != "" {
|
|
addr = u.Host + addr
|
|
}
|
|
return "unix", addr, nil
|
|
}
|
|
if strings.HasPrefix(ep, "tcp://") {
|
|
u, err := url.Parse(ep)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return "tcp", u.Host, nil
|
|
}
|
|
return "", "", fmt.Errorf("unsupported endpoint scheme: %s", ep)
|
|
}
|