Files
seaweedfs/weed/server/master_grpc_server_block_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

299 lines
8.0 KiB
Go

package weed_server
import (
"context"
"fmt"
"sync"
"sync/atomic"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
)
// testMasterServer creates a minimal MasterServer with mock VS calls for testing.
func testMasterServer(t *testing.T) *MasterServer {
t.Helper()
ms := &MasterServer{
blockRegistry: NewBlockVolumeRegistry(),
}
// Default mock: succeed with deterministic values.
ms.blockVSAllocate = func(ctx context.Context, server pb.ServerAddress, name string, sizeBytes uint64, diskType string) (string, string, string, error) {
return fmt.Sprintf("/data/%s.blk", name),
fmt.Sprintf("iqn.2024.test:%s", name),
string(server),
nil
}
ms.blockVSDelete = func(ctx context.Context, server pb.ServerAddress, name string) error {
return nil
}
return ms
}
func TestMaster_CreateBlockVolume(t *testing.T) {
ms := testMasterServer(t)
ms.blockRegistry.MarkBlockCapable("vs1:9333")
resp, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "test-vol",
SizeBytes: 1 << 30,
DiskType: "ssd",
})
if err != nil {
t.Fatalf("CreateBlockVolume: %v", err)
}
if resp.VolumeId != "test-vol" {
t.Fatalf("VolumeId: got %q, want test-vol", resp.VolumeId)
}
if resp.VolumeServer != "vs1:9333" {
t.Fatalf("VolumeServer: got %q, want vs1:9333", resp.VolumeServer)
}
if resp.Iqn == "" || resp.IscsiAddr == "" {
t.Fatal("IQN or ISCSIAddr is empty")
}
// Verify registry entry.
entry, ok := ms.blockRegistry.Lookup("test-vol")
if !ok {
t.Fatal("volume not found in registry")
}
if entry.Status != StatusActive {
t.Fatalf("status: got %d, want StatusActive", entry.Status)
}
}
func TestMaster_CreateIdempotent(t *testing.T) {
ms := testMasterServer(t)
ms.blockRegistry.MarkBlockCapable("vs1:9333")
resp1, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "vol1",
SizeBytes: 1 << 30,
})
if err != nil {
t.Fatalf("first create: %v", err)
}
resp2, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "vol1",
SizeBytes: 1 << 30,
})
if err != nil {
t.Fatalf("idempotent create: %v", err)
}
if resp1.VolumeId != resp2.VolumeId || resp1.VolumeServer != resp2.VolumeServer {
t.Fatalf("idempotent mismatch: %+v vs %+v", resp1, resp2)
}
}
func TestMaster_CreateIdempotentSizeMismatch(t *testing.T) {
ms := testMasterServer(t)
ms.blockRegistry.MarkBlockCapable("vs1:9333")
_, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "vol1",
SizeBytes: 1 << 30,
})
if err != nil {
t.Fatalf("first create: %v", err)
}
// Larger size should fail.
_, err = ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "vol1",
SizeBytes: 2 << 30,
})
if err == nil {
t.Fatal("expected error for size mismatch")
}
// Same or smaller size should succeed (idempotent).
_, err = ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "vol1",
SizeBytes: 1 << 29,
})
if err != nil {
t.Fatalf("smaller size should succeed: %v", err)
}
}
func TestMaster_CreateNoServers(t *testing.T) {
ms := testMasterServer(t)
// No block-capable servers registered.
_, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "vol1",
SizeBytes: 1 << 30,
})
if err == nil {
t.Fatal("expected error when no servers available")
}
}
func TestMaster_CreateVSFailure_Retry(t *testing.T) {
ms := testMasterServer(t)
ms.blockRegistry.MarkBlockCapable("vs1:9333")
ms.blockRegistry.MarkBlockCapable("vs2:9333")
var callCount atomic.Int32
ms.blockVSAllocate = func(ctx context.Context, server pb.ServerAddress, name string, sizeBytes uint64, diskType string) (string, string, string, error) {
n := callCount.Add(1)
if n == 1 {
return "", "", "", fmt.Errorf("disk full")
}
return fmt.Sprintf("/data/%s.blk", name),
fmt.Sprintf("iqn.2024.test:%s", name),
string(server), nil
}
resp, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "vol1",
SizeBytes: 1 << 30,
})
if err != nil {
t.Fatalf("expected retry to succeed: %v", err)
}
if resp.VolumeId != "vol1" {
t.Fatalf("VolumeId: got %q, want vol1", resp.VolumeId)
}
if callCount.Load() < 2 {
t.Fatalf("expected at least 2 VS calls, got %d", callCount.Load())
}
}
func TestMaster_CreateVSFailure_Cleanup(t *testing.T) {
ms := testMasterServer(t)
ms.blockRegistry.MarkBlockCapable("vs1:9333")
ms.blockVSAllocate = func(ctx context.Context, server pb.ServerAddress, name string, sizeBytes uint64, diskType string) (string, string, string, error) {
return "", "", "", fmt.Errorf("all servers broken")
}
_, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "vol1",
SizeBytes: 1 << 30,
})
if err == nil {
t.Fatal("expected error when all VS fail")
}
// No stale registry entry.
if _, ok := ms.blockRegistry.Lookup("vol1"); ok {
t.Fatal("stale registry entry should not exist")
}
}
func TestMaster_CreateConcurrentSameName(t *testing.T) {
ms := testMasterServer(t)
ms.blockRegistry.MarkBlockCapable("vs1:9333")
var callCount atomic.Int32
ms.blockVSAllocate = func(ctx context.Context, server pb.ServerAddress, name string, sizeBytes uint64, diskType string) (string, string, string, error) {
callCount.Add(1)
return fmt.Sprintf("/data/%s.blk", name),
fmt.Sprintf("iqn.2024.test:%s", name),
string(server), nil
}
var wg sync.WaitGroup
results := make([]*master_pb.CreateBlockVolumeResponse, 10)
errors := make([]error, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
results[i], errors[i] = ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "same-vol",
SizeBytes: 1 << 30,
})
}(i)
}
wg.Wait()
// Some may get "already in progress" error, but at least one must succeed.
successCount := 0
for i := 0; i < 10; i++ {
if errors[i] == nil {
successCount++
}
}
if successCount == 0 {
t.Fatal("at least one concurrent create should succeed")
}
// Only one VS allocation call should have been made.
if callCount.Load() != 1 {
t.Fatalf("expected exactly 1 VS call, got %d", callCount.Load())
}
}
func TestMaster_DeleteBlockVolume(t *testing.T) {
ms := testMasterServer(t)
ms.blockRegistry.MarkBlockCapable("vs1:9333")
_, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "vol1",
SizeBytes: 1 << 30,
})
if err != nil {
t.Fatalf("create: %v", err)
}
_, err = ms.DeleteBlockVolume(context.Background(), &master_pb.DeleteBlockVolumeRequest{
Name: "vol1",
})
if err != nil {
t.Fatalf("delete: %v", err)
}
if _, ok := ms.blockRegistry.Lookup("vol1"); ok {
t.Fatal("volume should be removed from registry")
}
}
func TestMaster_DeleteNotFound(t *testing.T) {
ms := testMasterServer(t)
_, err := ms.DeleteBlockVolume(context.Background(), &master_pb.DeleteBlockVolumeRequest{
Name: "nonexistent",
})
if err != nil {
t.Fatalf("delete nonexistent should succeed (idempotent): %v", err)
}
}
func TestMaster_LookupBlockVolume(t *testing.T) {
ms := testMasterServer(t)
ms.blockRegistry.MarkBlockCapable("vs1:9333")
_, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
Name: "vol1",
SizeBytes: 1 << 30,
})
if err != nil {
t.Fatalf("create: %v", err)
}
resp, err := ms.LookupBlockVolume(context.Background(), &master_pb.LookupBlockVolumeRequest{
Name: "vol1",
})
if err != nil {
t.Fatalf("lookup: %v", err)
}
if resp.VolumeServer != "vs1:9333" {
t.Fatalf("VolumeServer: got %q, want vs1:9333", resp.VolumeServer)
}
if resp.CapacityBytes != 1<<30 {
t.Fatalf("CapacityBytes: got %d, want %d", resp.CapacityBytes, 1<<30)
}
// Lookup nonexistent.
_, err = ms.LookupBlockVolume(context.Background(), &master_pb.LookupBlockVolumeRequest{
Name: "nonexistent",
})
if err == nil {
t.Fatal("lookup nonexistent should return error")
}
}