Files
seaweedfs/weed/storage/blockvol/iscsi/integration_test.go
T
Ping QiuandClaude Opus 4.6 09c7e40d29 feat: Phase 4A CP4a -- simulated master, assignment sequence tests, BlockVolumeStatus
Add SimulatedMaster test helper + 20 assignment sequence tests (8 sequence,
5 failover, 5 adversarial, 2 status). Add BlockVolumeStatus struct and
Status() method. Includes QA test files for CP1-CP4a. 940 total unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 09:33:45 -08:00

413 lines
11 KiB
Go

package iscsi_test
import (
"bytes"
"encoding/binary"
"io"
"log"
"net"
"path/filepath"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/iscsi"
)
// blockVolAdapter wraps a BlockVol to implement iscsi.BlockDevice.
type blockVolAdapter struct {
vol *blockvol.BlockVol
}
func (a *blockVolAdapter) ReadAt(lba uint64, length uint32) ([]byte, error) {
return a.vol.ReadLBA(lba, length)
}
func (a *blockVolAdapter) WriteAt(lba uint64, data []byte) error {
return a.vol.WriteLBA(lba, data)
}
func (a *blockVolAdapter) Trim(lba uint64, length uint32) error {
return a.vol.Trim(lba, length)
}
func (a *blockVolAdapter) SyncCache() error {
return a.vol.SyncCache()
}
func (a *blockVolAdapter) BlockSize() uint32 {
return a.vol.Info().BlockSize
}
func (a *blockVolAdapter) VolumeSize() uint64 {
return a.vol.Info().VolumeSize
}
func (a *blockVolAdapter) IsHealthy() bool {
return a.vol.Info().Healthy
}
const (
intTargetName = "iqn.2024.com.seaweedfs:integration"
intInitiatorName = "iqn.2024.com.test:client"
)
func createTestVol(t *testing.T) *blockvol.BlockVol {
t.Helper()
path := filepath.Join(t.TempDir(), "test.blk")
vol, err := blockvol.CreateBlockVol(path, blockvol.CreateOptions{
VolumeSize: 1024 * 4096, // 1024 blocks = 4MB
BlockSize: 4096,
WALSize: 1024 * 1024, // 1MB WAL
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { vol.Close() })
return vol
}
func setupIntegrationTarget(t *testing.T) (net.Conn, *iscsi.TargetServer) {
t.Helper()
vol := createTestVol(t)
adapter := &blockVolAdapter{vol: vol}
config := iscsi.DefaultTargetConfig()
config.TargetName = intTargetName
logger := log.New(io.Discard, "", 0)
ts := iscsi.NewTargetServer("127.0.0.1:0", config, logger)
ts.AddVolume(intTargetName, adapter)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
go ts.Serve(ln)
t.Cleanup(func() { ts.Close() })
conn, err := net.DialTimeout("tcp", ln.Addr().String(), 2*time.Second)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { conn.Close() })
// Login
params := iscsi.NewParams()
params.Set("InitiatorName", intInitiatorName)
params.Set("TargetName", intTargetName)
params.Set("SessionType", "Normal")
loginReq := &iscsi.PDU{}
loginReq.SetOpcode(iscsi.OpLoginReq)
loginReq.SetLoginStages(iscsi.StageSecurityNeg, iscsi.StageFullFeature)
loginReq.SetLoginTransit(true)
loginReq.SetISID([6]byte{0x00, 0x02, 0x3D, 0x00, 0x00, 0x01})
loginReq.SetCmdSN(1)
loginReq.DataSegment = params.Encode()
if err := iscsi.WritePDU(conn, loginReq); err != nil {
t.Fatal(err)
}
resp, err := iscsi.ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.LoginStatusClass() != iscsi.LoginStatusSuccess {
t.Fatalf("login failed: %d/%d", resp.LoginStatusClass(), resp.LoginStatusDetail())
}
return conn, ts
}
func TestIntegration(t *testing.T) {
tests := []struct {
name string
run func(t *testing.T)
}{
{"write_read_verify", testWriteReadVerify},
{"multi_block_write_read", testMultiBlockWriteRead},
{"inquiry_capacity", testInquiryCapacity},
{"sync_cache", testIntSyncCache},
{"test_unit_ready", testIntTestUnitReady},
{"concurrent_readers_writers", testConcurrentReadersWriters},
{"write_at_boundary", testWriteAtBoundary},
{"unmap_integration", testUnmapIntegration},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.run(t)
})
}
}
func sendSCSICmd(t *testing.T, conn net.Conn, cdb [16]byte, cmdSN uint32, read bool, write bool, dataOut []byte, expLen uint32) *iscsi.PDU {
t.Helper()
cmd := &iscsi.PDU{}
cmd.SetOpcode(iscsi.OpSCSICmd)
flags := uint8(iscsi.FlagF)
if read {
flags |= iscsi.FlagR
}
if write {
flags |= iscsi.FlagW
}
cmd.SetOpSpecific1(flags)
cmd.SetInitiatorTaskTag(cmdSN)
cmd.SetExpectedDataTransferLength(expLen)
cmd.SetCmdSN(cmdSN)
cmd.SetCDB(cdb)
if dataOut != nil {
cmd.DataSegment = dataOut
}
if err := iscsi.WritePDU(conn, cmd); err != nil {
t.Fatal(err)
}
resp, err := iscsi.ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
return resp
}
func testWriteReadVerify(t *testing.T) {
conn, _ := setupIntegrationTarget(t)
// Write pattern to LBA 0
writeData := make([]byte, 4096)
for i := range writeData {
writeData[i] = byte(i % 251) // prime modulus for pattern
}
var wCDB [16]byte
wCDB[0] = iscsi.ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], 0) // LBA 0
binary.BigEndian.PutUint16(wCDB[7:9], 1) // 1 block
resp := sendSCSICmd(t, conn, wCDB, 2, false, true, writeData, 4096)
if resp.SCSIStatus() != iscsi.SCSIStatusGood {
t.Fatalf("write failed: status %d", resp.SCSIStatus())
}
// Read it back
var rCDB [16]byte
rCDB[0] = iscsi.ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], 0)
binary.BigEndian.PutUint16(rCDB[7:9], 1)
resp2 := sendSCSICmd(t, conn, rCDB, 3, true, false, nil, 4096)
if resp2.Opcode() != iscsi.OpSCSIDataIn {
t.Fatalf("expected Data-In, got %s", iscsi.OpcodeName(resp2.Opcode()))
}
if !bytes.Equal(resp2.DataSegment, writeData) {
t.Fatal("data integrity verification failed")
}
}
func testMultiBlockWriteRead(t *testing.T) {
conn, _ := setupIntegrationTarget(t)
// Write 4 blocks at LBA 10
blockCount := uint16(4)
dataLen := int(blockCount) * 4096
writeData := make([]byte, dataLen)
for i := range writeData {
writeData[i] = byte((i / 4096) + 1) // each block has different fill
}
var wCDB [16]byte
wCDB[0] = iscsi.ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], 10)
binary.BigEndian.PutUint16(wCDB[7:9], blockCount)
resp := sendSCSICmd(t, conn, wCDB, 2, false, true, writeData, uint32(dataLen))
if resp.SCSIStatus() != iscsi.SCSIStatusGood {
t.Fatalf("write failed: status %d", resp.SCSIStatus())
}
// Read back all 4 blocks
var rCDB [16]byte
rCDB[0] = iscsi.ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], 10)
binary.BigEndian.PutUint16(rCDB[7:9], blockCount)
resp2 := sendSCSICmd(t, conn, rCDB, 3, true, false, nil, uint32(dataLen))
// May be split across multiple Data-In PDUs -- reassemble
var readData []byte
readData = append(readData, resp2.DataSegment...)
// If first PDU doesn't have S-bit, read more
for resp2.OpSpecific1()&iscsi.FlagS == 0 {
var err error
resp2, err = iscsi.ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
readData = append(readData, resp2.DataSegment...)
}
if !bytes.Equal(readData, writeData) {
t.Fatal("multi-block data integrity failed")
}
}
func testInquiryCapacity(t *testing.T) {
conn, _ := setupIntegrationTarget(t)
// TEST UNIT READY
var tuCDB [16]byte
tuCDB[0] = iscsi.ScsiTestUnitReady
resp := sendSCSICmd(t, conn, tuCDB, 2, false, false, nil, 0)
if resp.SCSIStatus() != iscsi.SCSIStatusGood {
t.Fatal("test unit ready failed")
}
// INQUIRY
var iqCDB [16]byte
iqCDB[0] = iscsi.ScsiInquiry
binary.BigEndian.PutUint16(iqCDB[3:5], 96)
resp = sendSCSICmd(t, conn, iqCDB, 3, true, false, nil, 96)
if resp.Opcode() != iscsi.OpSCSIDataIn {
t.Fatal("expected Data-In for inquiry")
}
if resp.DataSegment[0] != 0x00 { // SBC device
t.Fatal("not SBC device type")
}
// READ CAPACITY 10
var rcCDB [16]byte
rcCDB[0] = iscsi.ScsiReadCapacity10
resp = sendSCSICmd(t, conn, rcCDB, 4, true, false, nil, 8)
if resp.Opcode() != iscsi.OpSCSIDataIn {
t.Fatal("expected Data-In for read capacity")
}
lastLBA := binary.BigEndian.Uint32(resp.DataSegment[0:4])
blockSize := binary.BigEndian.Uint32(resp.DataSegment[4:8])
if lastLBA != 1023 { // 1024 blocks, last = 1023
t.Fatalf("last LBA: %d, expected 1023", lastLBA)
}
if blockSize != 4096 {
t.Fatalf("block size: %d", blockSize)
}
}
func testIntSyncCache(t *testing.T) {
conn, _ := setupIntegrationTarget(t)
var cdb [16]byte
cdb[0] = iscsi.ScsiSyncCache10
resp := sendSCSICmd(t, conn, cdb, 2, false, false, nil, 0)
if resp.SCSIStatus() != iscsi.SCSIStatusGood {
t.Fatalf("sync cache failed: %d", resp.SCSIStatus())
}
}
func testIntTestUnitReady(t *testing.T) {
conn, _ := setupIntegrationTarget(t)
var cdb [16]byte
cdb[0] = iscsi.ScsiTestUnitReady
resp := sendSCSICmd(t, conn, cdb, 2, false, false, nil, 0)
if resp.SCSIStatus() != iscsi.SCSIStatusGood {
t.Fatal("TUR failed")
}
}
func testConcurrentReadersWriters(t *testing.T) {
conn, _ := setupIntegrationTarget(t)
// Sequential writes to different LBAs, then sequential reads to verify
cmdSN := uint32(2)
for lba := uint32(0); lba < 10; lba++ {
data := bytes.Repeat([]byte{byte(lba)}, 4096)
var wCDB [16]byte
wCDB[0] = iscsi.ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], lba)
binary.BigEndian.PutUint16(wCDB[7:9], 1)
resp := sendSCSICmd(t, conn, wCDB, cmdSN, false, true, data, 4096)
if resp.SCSIStatus() != iscsi.SCSIStatusGood {
t.Fatalf("write LBA %d failed", lba)
}
cmdSN++
}
// Read back and verify
for lba := uint32(0); lba < 10; lba++ {
var rCDB [16]byte
rCDB[0] = iscsi.ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], lba)
binary.BigEndian.PutUint16(rCDB[7:9], 1)
resp := sendSCSICmd(t, conn, rCDB, cmdSN, true, false, nil, 4096)
if resp.Opcode() != iscsi.OpSCSIDataIn {
t.Fatalf("LBA %d: expected Data-In", lba)
}
expected := bytes.Repeat([]byte{byte(lba)}, 4096)
if !bytes.Equal(resp.DataSegment, expected) {
t.Fatalf("LBA %d: data mismatch", lba)
}
cmdSN++
}
}
func testWriteAtBoundary(t *testing.T) {
conn, _ := setupIntegrationTarget(t)
// Write at last valid LBA (1023)
data := bytes.Repeat([]byte{0xFF}, 4096)
var wCDB [16]byte
wCDB[0] = iscsi.ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], 1023)
binary.BigEndian.PutUint16(wCDB[7:9], 1)
resp := sendSCSICmd(t, conn, wCDB, 2, false, true, data, 4096)
if resp.SCSIStatus() != iscsi.SCSIStatusGood {
t.Fatal("boundary write failed")
}
// Write past end -- should fail
var wCDB2 [16]byte
wCDB2[0] = iscsi.ScsiWrite10
binary.BigEndian.PutUint32(wCDB2[2:6], 1024) // out of bounds
binary.BigEndian.PutUint16(wCDB2[7:9], 1)
resp2 := sendSCSICmd(t, conn, wCDB2, 3, false, true, data, 4096)
if resp2.SCSIStatus() == iscsi.SCSIStatusGood {
t.Fatal("OOB write should fail")
}
}
func testUnmapIntegration(t *testing.T) {
conn, _ := setupIntegrationTarget(t)
// Write data at LBA 5
writeData := bytes.Repeat([]byte{0xCC}, 4096)
var wCDB [16]byte
wCDB[0] = iscsi.ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], 5)
binary.BigEndian.PutUint16(wCDB[7:9], 1)
sendSCSICmd(t, conn, wCDB, 2, false, true, writeData, 4096)
// UNMAP LBA 5
unmapData := make([]byte, 24)
binary.BigEndian.PutUint16(unmapData[0:2], 22)
binary.BigEndian.PutUint16(unmapData[2:4], 16)
binary.BigEndian.PutUint64(unmapData[8:16], 5)
binary.BigEndian.PutUint32(unmapData[16:20], 1)
var uCDB [16]byte
uCDB[0] = iscsi.ScsiUnmap
resp := sendSCSICmd(t, conn, uCDB, 3, false, true, unmapData, uint32(len(unmapData)))
if resp.SCSIStatus() != iscsi.SCSIStatusGood {
t.Fatalf("unmap failed: %d", resp.SCSIStatus())
}
// Read back -- should be zeros
var rCDB [16]byte
rCDB[0] = iscsi.ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], 5)
binary.BigEndian.PutUint16(rCDB[7:9], 1)
resp2 := sendSCSICmd(t, conn, rCDB, 4, true, false, nil, 4096)
if resp2.Opcode() != iscsi.OpSCSIDataIn {
t.Fatal("expected Data-In")
}
zeros := make([]byte, 4096)
if !bytes.Equal(resp2.DataSegment, zeros) {
t.Fatal("unmapped block should return zeros")
}
}