Files
seaweedfs/weed/storage/blockvol/csi/volume_manager_test.go
T
Ping QiuandClaude Opus 4.6 5a9a52f2d0 feat: Phase 6 CP6-2 -- CSI control-plane integration + csi-sanity/k3s validation
CP6-2 wires the CSI driver to SeaweedFS master/volume-server control plane:
- Proto: block volume messages in master.proto/volume_server.proto, codegen
- Master registry: in-memory BlockVolumeRegistry with Pending->Active status,
  full/delta heartbeat, inflight lock, placement (fewest volumes)
- VS gRPC: AllocateBlockVolume/DeleteBlockVolume handlers, shared naming
- Master RPCs: CreateBlockVolume (retry up to 3 servers), Delete, Lookup
- Heartbeat: block volume fields wired into bidirectional stream
- CSI Controller: VolumeBackend interface (Local + Master), returns volume_context
- CSI Node: reads volume_context for remote targets, staged map + IQN derivation
- Mode flag: --mode=controller/node/all, --master for control-plane
- K8s manifests: csi-driver.yaml, csi-controller.yaml, csi-node.yaml

csi-sanity conformance (33 pass, 58 skip) found 6 bugs:
- BUG-SANITY-1/2/3: missing VolumeCapabilities/VolumeCapability validation
- BUG-SANITY-4: NodePublish used mount instead of bind mount
- BUG-SANITY-5: NodeUnpublish didn't remove target path
- BUG-SANITY-6: NodeUnpublish failed on unmounted path

k3s Level 4 (PVC->Pod data persistence) found 1 bug:
- BUG-K3S-1: IsLoggedIn didn't handle iscsiadm exit code 21

226 CSI tests + 54 server tests = 280 new tests, all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:01:08 -08:00

232 lines
5.9 KiB
Go

package csi
import (
"context"
"log"
"os"
"path/filepath"
"sync"
"testing"
)
func newTestManager(t *testing.T) *VolumeManager {
t.Helper()
dir := t.TempDir()
logger := log.New(os.Stderr, "[test-vm] ", log.LstdFlags)
mgr := NewVolumeManager(dir, "127.0.0.1:0", "iqn.2024.com.seaweedfs", logger)
if err := mgr.Start(context.Background()); err != nil {
t.Fatalf("start: %v", err)
}
t.Cleanup(func() { mgr.Stop() })
return mgr
}
func TestVolumeManager_CreateOpenClose(t *testing.T) {
mgr := newTestManager(t)
if err := mgr.CreateVolume("vol1", 4*1024*1024); err != nil {
t.Fatalf("create: %v", err)
}
if !mgr.VolumeExists("vol1") {
t.Fatal("expected vol1 to exist after create")
}
iqn := mgr.VolumeIQN("vol1")
if iqn == "" {
t.Fatal("expected non-empty IQN")
}
// Close
if err := mgr.CloseVolume("vol1"); err != nil {
t.Fatalf("close: %v", err)
}
if mgr.VolumeExists("vol1") {
t.Fatal("expected vol1 to not exist after close")
}
// Reopen
if err := mgr.OpenVolume("vol1"); err != nil {
t.Fatalf("reopen: %v", err)
}
if !mgr.VolumeExists("vol1") {
t.Fatal("expected vol1 to exist after reopen")
}
}
func TestVolumeManager_DeleteRemovesFile(t *testing.T) {
mgr := newTestManager(t)
if err := mgr.CreateVolume("delvol", 4*1024*1024); err != nil {
t.Fatalf("create: %v", err)
}
volPath := filepath.Join(mgr.dataDir, "delvol.blk")
if _, err := os.Stat(volPath); err != nil {
t.Fatalf("expected file to exist: %v", err)
}
if err := mgr.DeleteVolume("delvol"); err != nil {
t.Fatalf("delete: %v", err)
}
if _, err := os.Stat(volPath); !os.IsNotExist(err) {
t.Fatalf("expected file to be removed, got: %v", err)
}
if mgr.VolumeExists("delvol") {
t.Fatal("expected volume to not exist after delete")
}
}
func TestVolumeManager_DuplicateCreate(t *testing.T) {
mgr := newTestManager(t)
if err := mgr.CreateVolume("dup", 4*1024*1024); err != nil {
t.Fatalf("first create: %v", err)
}
// Same size -> idempotent success.
if err := mgr.CreateVolume("dup", 4*1024*1024); err != nil {
t.Fatalf("duplicate create (same size): expected success, got: %v", err)
}
// Larger size -> mismatch error.
err := mgr.CreateVolume("dup", 8*1024*1024)
if err != ErrVolumeSizeMismatch {
t.Fatalf("expected ErrVolumeSizeMismatch, got: %v", err)
}
}
func TestVolumeManager_ListenAddr(t *testing.T) {
mgr := newTestManager(t)
addr := mgr.ListenAddr()
if addr == "" {
t.Fatal("expected non-empty listen addr")
}
}
func TestVolumeManager_OpenNonExistent(t *testing.T) {
mgr := newTestManager(t)
err := mgr.OpenVolume("nonexistent")
if err == nil {
t.Fatal("expected error opening non-existent volume")
}
}
func TestVolumeManager_CloseAlreadyClosed(t *testing.T) {
mgr := newTestManager(t)
// Close a volume that was never opened -- should be idempotent.
if err := mgr.CloseVolume("nope"); err != nil {
t.Fatalf("close non-existent: %v", err)
}
}
func TestVolumeManager_ConcurrentCreateDelete(t *testing.T) {
mgr := newTestManager(t)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
name := "conc" + string(rune('0'+i))
wg.Add(1)
go func(n string) {
defer wg.Done()
if err := mgr.CreateVolume(n, 4*1024*1024); err != nil {
t.Errorf("create %s: %v", n, err)
return
}
if err := mgr.DeleteVolume(n); err != nil {
t.Errorf("delete %s: %v", n, err)
}
}(name)
}
wg.Wait()
}
func TestVolumeManager_SanitizeIQN(t *testing.T) {
tests := []struct {
input string
want string
}{
{"pvc-abc123", "pvc-abc123"},
{"PVC_ABC123", "pvc-abc123"},
{"hello world!", "hello-world-"},
{"a/b\\c:d", "a-b-c-d"},
}
for _, tt := range tests {
got := SanitizeIQN(tt.input)
if got != tt.want {
t.Errorf("SanitizeIQN(%q): got %q, want %q", tt.input, got, tt.want)
}
}
// Test truncation to 64 chars.
long := ""
for i := 0; i < 100; i++ {
long += "a"
}
if len(SanitizeIQN(long)) != 64 {
t.Fatalf("expected truncation to 64, got %d", len(SanitizeIQN(long)))
}
}
// TestVolumeManager_CreateIdempotentAfterRestart simulates driver restart:
// existing .blk file on disk but not tracked in-memory.
func TestVolumeManager_CreateIdempotentAfterRestart(t *testing.T) {
dir := t.TempDir()
logger := log.New(os.Stderr, "[test-vm] ", log.LstdFlags)
// Phase 1: create a volume, then stop the manager.
mgr1 := NewVolumeManager(dir, "127.0.0.1:0", "iqn.2024.com.seaweedfs", logger)
if err := mgr1.Start(context.Background()); err != nil {
t.Fatalf("start1: %v", err)
}
if err := mgr1.CreateVolume("restart-vol", 4*1024*1024); err != nil {
t.Fatalf("create: %v", err)
}
mgr1.Stop()
// Verify .blk file still exists on disk.
volPath := filepath.Join(dir, "restart-vol.blk")
if _, err := os.Stat(volPath); err != nil {
t.Fatalf("expected .blk file to exist: %v", err)
}
// Phase 2: new manager (simulates restart) -- CreateVolume should
// adopt the existing file and return success.
mgr2 := NewVolumeManager(dir, "127.0.0.1:0", "iqn.2024.com.seaweedfs", logger)
if err := mgr2.Start(context.Background()); err != nil {
t.Fatalf("start2: %v", err)
}
defer mgr2.Stop()
if err := mgr2.CreateVolume("restart-vol", 4*1024*1024); err != nil {
t.Fatalf("create after restart: expected idempotent success, got: %v", err)
}
if !mgr2.VolumeExists("restart-vol") {
t.Fatal("expected volume to be tracked after adoption")
}
if mgr2.VolumeSizeBytes("restart-vol") < 4*1024*1024 {
t.Fatalf("expected size >= 4MiB, got %d", mgr2.VolumeSizeBytes("restart-vol"))
}
}
// TestVolumeManager_IQNCollision verifies that two long names sharing a prefix
// produce distinct IQNs after truncation.
func TestVolumeManager_IQNCollision(t *testing.T) {
prefix := ""
for i := 0; i < 70; i++ {
prefix += "a"
}
name1 := prefix + "-suffix1"
name2 := prefix + "-suffix2"
iqn1 := SanitizeIQN(name1)
iqn2 := SanitizeIQN(name2)
if iqn1 == iqn2 {
t.Fatalf("IQN collision: both names produced %q", iqn1)
}
if len(iqn1) > 64 || len(iqn2) > 64 {
t.Fatalf("IQN too long: %d, %d", len(iqn1), len(iqn2))
}
}