mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
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>
232 lines
5.9 KiB
Go
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))
|
|
}
|
|
}
|