Files
seaweedfs/weed/storage/blockvol/csi/server.go
T
Ping QiuandClaude Opus 4.6 bbadeeb89b feat: Phase 10 CP10-2 -- CSI NVMe/TCP node plugin, 210 tests
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>
2026-03-08 23:02:59 -07:00

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)
}