mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-25 01:14:50 +00:00
Add HTTP admin server to iscsi-target binary (POST /assign, GET /status, POST /replica, POST /rebuild) and 7 HA integration tests validating failover, split-brain prevention, epoch fencing, and demote-under-IO. New files: - admin.go: HTTP admin endpoint with input validation - ha_target.go: HATarget helper wrapping Target + admin HTTP calls - ha_test.go: 7 HA tests (all PASS on WSL2, 67.7s total) Bug fixes: - BUG-CP4B4-1: CmdSN init (expCmdSN=0 not 1, first SCSI cmd was dropped) - BUG-CP4B4-2: RoleNone->RoleReplica missing SetEpoch (WAL rejected) - BUG-CP4B4-3: replica applyEntry didn't update vol.nextLSN (status=0) - BUG-CP4B4-4: PID discovery killed primary instead of replica (shared binPath; fixed by grepping volFile) - BUG-CP4B4-5: artifact collector overwrote primary log with replica log (added CollectLabeled method) Also: 3s write deadline on WAL shipper data connection to avoid 120s TCP retransmission timeout when replica is dead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1140 lines
28 KiB
Go
1140 lines
28 KiB
Go
package iscsi
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// testResolver implements TargetResolver, TargetLister, and DeviceLookup.
|
|
type testResolver struct {
|
|
targets []DiscoveryTarget
|
|
dev BlockDevice
|
|
}
|
|
|
|
func (r *testResolver) HasTarget(name string) bool {
|
|
for _, t := range r.targets {
|
|
if t.Name == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r *testResolver) ListTargets() []DiscoveryTarget {
|
|
return r.targets
|
|
}
|
|
|
|
func (r *testResolver) LookupDevice(iqn string) BlockDevice {
|
|
if r.dev != nil {
|
|
return r.dev
|
|
}
|
|
return &nullDevice{}
|
|
}
|
|
|
|
func newTestResolver() *testResolver {
|
|
return newTestResolverWithDevice(nil)
|
|
}
|
|
|
|
func newTestResolverWithDevice(dev BlockDevice) *testResolver {
|
|
return &testResolver{
|
|
targets: []DiscoveryTarget{
|
|
{Name: testTargetName, Address: "127.0.0.1:3260,1"},
|
|
},
|
|
dev: dev,
|
|
}
|
|
}
|
|
|
|
func TestSession(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
run func(t *testing.T)
|
|
}{
|
|
{"login_and_read", testLoginAndRead},
|
|
{"login_and_write", testLoginAndWrite},
|
|
{"nop_ping", testNOPPing},
|
|
{"logout", testLogout},
|
|
{"discovery_session", testDiscoverySession},
|
|
{"task_mgmt", testTaskMgmt},
|
|
{"reject_scsi_before_login", testRejectSCSIBeforeLogin},
|
|
{"connection_close", testConnectionClose},
|
|
// Phase 3 CP2: RX/TX split tests.
|
|
{"rxtx_read_write_basic", testRXTXReadWriteBasic},
|
|
{"rxtx_pipelined_reads", testRXTXPipelinedReads},
|
|
{"rxtx_pipelined_writes", testRXTXPipelinedWrites},
|
|
{"rxtx_write_with_r2t", testRXTXWriteWithR2T},
|
|
{"rxtx_mixed_ops", testRXTXMixedOps},
|
|
{"rxtx_large_read_datain", testRXTXLargeReadDataIn},
|
|
{"rxtx_statsn_monotonic", testRXTXStatSNMonotonic},
|
|
{"rxtx_statsn_datain_no_increment", testRXTXStatSNDataInNoIncrement},
|
|
{"rxtx_statsn_mixed_types", testRXTXStatSNMixedTypes},
|
|
{"rxtx_shutdown_clean", testRXTXShutdownClean},
|
|
{"rxtx_conn_drop_reader", testRXTXConnDropReader},
|
|
{"rxtx_conn_drop_writer", testRXTXConnDropWriter},
|
|
// Phase 3 code review fixes.
|
|
{"rxtx_r2t_statsn_fresh", testRXTXR2TStatSNFresh},
|
|
{"rxtx_tx_error_exits_clean", testRXTXTxErrorExitsClean},
|
|
{"rxtx_login_phase_reject", testRXTXLoginPhaseReject},
|
|
// Phase 3 bug fixes (P3-BUG-2, P3-BUG-3).
|
|
{"rxtx_pending_queue_overflow", testRXTXPendingQueueOverflow},
|
|
{"rxtx_dataout_timeout", testRXTXDataOutTimeout},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.run(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
type sessionTestEnv struct {
|
|
clientConn net.Conn
|
|
session *Session
|
|
done chan error
|
|
}
|
|
|
|
func setupSession(t *testing.T) *sessionTestEnv {
|
|
return setupSessionWithConfig(t, nil)
|
|
}
|
|
|
|
func setupSessionWithConfig(t *testing.T, cfgFn func(*TargetConfig)) *sessionTestEnv {
|
|
t.Helper()
|
|
client, server := net.Pipe()
|
|
|
|
dev := newMockDevice(1024 * 4096) // 1024 blocks
|
|
config := DefaultTargetConfig()
|
|
config.TargetName = testTargetName
|
|
if cfgFn != nil {
|
|
cfgFn(&config)
|
|
}
|
|
resolver := newTestResolverWithDevice(dev)
|
|
logger := log.New(io.Discard, "", 0)
|
|
|
|
sess := NewSession(server, config, resolver, resolver, logger)
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- sess.HandleConnection()
|
|
}()
|
|
|
|
t.Cleanup(func() {
|
|
sess.Close()
|
|
client.Close()
|
|
})
|
|
|
|
return &sessionTestEnv{clientConn: client, session: sess, done: done}
|
|
}
|
|
|
|
func doLogin(t *testing.T, conn net.Conn) {
|
|
t.Helper()
|
|
params := NewParams()
|
|
params.Set("InitiatorName", testInitiatorName)
|
|
params.Set("TargetName", testTargetName)
|
|
params.Set("SessionType", "Normal")
|
|
|
|
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
|
|
req.SetCmdSN(1)
|
|
|
|
if err := WritePDU(conn, req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := ReadPDU(conn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpLoginResp {
|
|
t.Fatalf("expected LoginResp, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
if resp.LoginStatusClass() != LoginStatusSuccess {
|
|
t.Fatalf("login failed: %d/%d", resp.LoginStatusClass(), resp.LoginStatusDetail())
|
|
}
|
|
}
|
|
|
|
func testLoginAndRead(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Send SCSI READ_10 for 1 block at LBA 0
|
|
cmd := &PDU{}
|
|
cmd.SetOpcode(OpSCSICmd)
|
|
cmd.SetOpSpecific1(FlagF | FlagR) // Final + Read
|
|
cmd.SetInitiatorTaskTag(1)
|
|
cmd.SetExpectedDataTransferLength(4096)
|
|
cmd.SetCmdSN(0)
|
|
cmd.SetExpStatSN(2)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiRead10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 0) // LBA=0
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1) // 1 block
|
|
cmd.SetCDB(cdb)
|
|
|
|
if err := WritePDU(env.clientConn, cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Expect Data-In with S-bit (single PDU, 4096 bytes of zeros)
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpSCSIDataIn {
|
|
t.Fatalf("expected Data-In, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
if resp.OpSpecific1()&FlagS == 0 {
|
|
t.Fatal("S-bit not set")
|
|
}
|
|
if len(resp.DataSegment) != 4096 {
|
|
t.Fatalf("data length: %d", len(resp.DataSegment))
|
|
}
|
|
}
|
|
|
|
func testLoginAndWrite(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// SCSI WRITE_10: 1 block at LBA 5 with immediate data
|
|
cmd := &PDU{}
|
|
cmd.SetOpcode(OpSCSICmd)
|
|
cmd.SetOpSpecific1(FlagF | FlagW) // Final + Write
|
|
cmd.SetInitiatorTaskTag(1)
|
|
cmd.SetExpectedDataTransferLength(4096)
|
|
cmd.SetCmdSN(0)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 5)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
cmd.SetCDB(cdb)
|
|
|
|
// Include immediate data
|
|
cmd.DataSegment = bytes.Repeat([]byte{0xAA}, 4096)
|
|
|
|
if err := WritePDU(env.clientConn, cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Expect SCSI Response (good)
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpSCSIResp {
|
|
t.Fatalf("expected SCSI Response, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
if resp.SCSIStatus() != SCSIStatusGood {
|
|
t.Fatalf("status: %d", resp.SCSIStatus())
|
|
}
|
|
}
|
|
|
|
func testNOPPing(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Send NOP-Out
|
|
nop := &PDU{}
|
|
nop.SetOpcode(OpNOPOut)
|
|
nop.SetOpSpecific1(FlagF)
|
|
nop.SetInitiatorTaskTag(0x9999)
|
|
nop.SetImmediate(true)
|
|
nop.DataSegment = []byte("ping")
|
|
|
|
if err := WritePDU(env.clientConn, nop); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpNOPIn {
|
|
t.Fatalf("expected NOP-In, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
if resp.InitiatorTaskTag() != 0x9999 {
|
|
t.Fatal("ITT mismatch")
|
|
}
|
|
if string(resp.DataSegment) != "ping" {
|
|
t.Fatalf("echo data: %q", resp.DataSegment)
|
|
}
|
|
}
|
|
|
|
func testLogout(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Send Logout
|
|
logout := &PDU{}
|
|
logout.SetOpcode(OpLogoutReq)
|
|
logout.SetOpSpecific1(FlagF)
|
|
logout.SetInitiatorTaskTag(0xAAAA)
|
|
logout.SetCmdSN(0)
|
|
|
|
if err := WritePDU(env.clientConn, logout); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpLogoutResp {
|
|
t.Fatalf("expected Logout Response, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
|
|
// After logout, the connection should be closed by the target.
|
|
// Verify by trying to read -- should get EOF.
|
|
_, err = ReadPDU(env.clientConn)
|
|
if err == nil {
|
|
t.Fatal("expected EOF after logout")
|
|
}
|
|
}
|
|
|
|
func testDiscoverySession(t *testing.T) {
|
|
env := setupSession(t)
|
|
|
|
// Login as discovery session
|
|
params := NewParams()
|
|
params.Set("InitiatorName", testInitiatorName)
|
|
params.Set("SessionType", "Discovery")
|
|
|
|
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
|
|
req.SetCmdSN(1)
|
|
WritePDU(env.clientConn, req)
|
|
ReadPDU(env.clientConn) // login resp
|
|
|
|
// Send SendTargets=All
|
|
textParams := NewParams()
|
|
textParams.Set("SendTargets", "All")
|
|
textReq := makeTextReq(textParams)
|
|
textReq.SetCmdSN(0)
|
|
|
|
if err := WritePDU(env.clientConn, textReq); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpTextResp {
|
|
t.Fatalf("expected Text Response, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
body := string(resp.DataSegment)
|
|
if len(body) == 0 {
|
|
t.Fatal("empty discovery response")
|
|
}
|
|
}
|
|
|
|
func testTaskMgmt(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
tm := &PDU{}
|
|
tm.SetOpcode(OpSCSITaskMgmt)
|
|
tm.SetOpSpecific1(FlagF | 0x01) // ABORT TASK
|
|
tm.SetInitiatorTaskTag(0xBBBB)
|
|
tm.SetImmediate(true)
|
|
|
|
if err := WritePDU(env.clientConn, tm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpSCSITaskResp {
|
|
t.Fatalf("expected Task Mgmt Response, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
}
|
|
|
|
func testRejectSCSIBeforeLogin(t *testing.T) {
|
|
env := setupSession(t)
|
|
|
|
// Send SCSI command without login
|
|
cmd := &PDU{}
|
|
cmd.SetOpcode(OpSCSICmd)
|
|
cmd.SetOpSpecific1(FlagF | FlagR)
|
|
cmd.SetInitiatorTaskTag(1)
|
|
|
|
if err := WritePDU(env.clientConn, cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpReject {
|
|
t.Fatalf("expected Reject, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
}
|
|
|
|
func testConnectionClose(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Close client side
|
|
env.clientConn.Close()
|
|
|
|
select {
|
|
case err := <-env.done:
|
|
if err != nil {
|
|
t.Fatalf("unexpected error on clean close: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("session did not detect close")
|
|
}
|
|
}
|
|
|
|
// --- Phase 3 CP2: RX/TX split tests ---
|
|
|
|
func sendSCSIRead(t *testing.T, conn net.Conn, lba uint32, blocks uint16, itt uint32, cmdSN uint32) {
|
|
t.Helper()
|
|
cmd := &PDU{}
|
|
cmd.SetOpcode(OpSCSICmd)
|
|
cmd.SetOpSpecific1(FlagF | FlagR)
|
|
cmd.SetInitiatorTaskTag(itt)
|
|
cmd.SetExpectedDataTransferLength(uint32(blocks) * 4096)
|
|
cmd.SetCmdSN(cmdSN)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiRead10
|
|
binary.BigEndian.PutUint32(cdb[2:6], lba)
|
|
binary.BigEndian.PutUint16(cdb[7:9], blocks)
|
|
cmd.SetCDB(cdb)
|
|
|
|
if err := WritePDU(conn, cmd); err != nil {
|
|
t.Fatalf("sendSCSIRead: %v", err)
|
|
}
|
|
}
|
|
|
|
func sendSCSIWriteImmediate(t *testing.T, conn net.Conn, lba uint32, data []byte, itt uint32, cmdSN uint32) {
|
|
t.Helper()
|
|
cmd := &PDU{}
|
|
cmd.SetOpcode(OpSCSICmd)
|
|
cmd.SetOpSpecific1(FlagF | FlagW)
|
|
cmd.SetInitiatorTaskTag(itt)
|
|
cmd.SetExpectedDataTransferLength(uint32(len(data)))
|
|
cmd.SetCmdSN(cmdSN)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], lba)
|
|
binary.BigEndian.PutUint16(cdb[7:9], uint16(len(data)/4096))
|
|
cmd.SetCDB(cdb)
|
|
cmd.DataSegment = data
|
|
|
|
if err := WritePDU(conn, cmd); err != nil {
|
|
t.Fatalf("sendSCSIWriteImmediate: %v", err)
|
|
}
|
|
}
|
|
|
|
func testRXTXReadWriteBasic(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Write then read via the RX/TX split path.
|
|
data := bytes.Repeat([]byte{0xBB}, 4096)
|
|
sendSCSIWriteImmediate(t, env.clientConn, 0, data, 1, 2)
|
|
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpSCSIResp || resp.SCSIStatus() != SCSIStatusGood {
|
|
t.Fatalf("write resp: opcode=%s status=%d", OpcodeName(resp.Opcode()), resp.SCSIStatus())
|
|
}
|
|
|
|
sendSCSIRead(t, env.clientConn, 0, 1, 2, 3)
|
|
resp, err = ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpSCSIDataIn {
|
|
t.Fatalf("expected DataIn, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
if !bytes.Equal(resp.DataSegment, data) {
|
|
t.Error("read data mismatch")
|
|
}
|
|
}
|
|
|
|
func testRXTXPipelinedReads(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Send 4 reads back-to-back.
|
|
for i := uint32(0); i < 4; i++ {
|
|
sendSCSIRead(t, env.clientConn, i, 1, i+1, i+2)
|
|
}
|
|
|
|
// Receive all 4 responses.
|
|
for i := 0; i < 4; i++ {
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatalf("read %d: %v", i, err)
|
|
}
|
|
if resp.Opcode() != OpSCSIDataIn {
|
|
t.Fatalf("read %d: expected DataIn, got %s", i, OpcodeName(resp.Opcode()))
|
|
}
|
|
}
|
|
}
|
|
|
|
func testRXTXPipelinedWrites(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Send 8 writes back-to-back with immediate data.
|
|
for i := uint32(0); i < 8; i++ {
|
|
data := bytes.Repeat([]byte{byte(0xA0 + i)}, 4096)
|
|
sendSCSIWriteImmediate(t, env.clientConn, i, data, i+1, i+2)
|
|
}
|
|
|
|
// Receive all 8 responses.
|
|
for i := 0; i < 8; i++ {
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatalf("write %d: %v", i, err)
|
|
}
|
|
if resp.Opcode() != OpSCSIResp {
|
|
t.Fatalf("write %d: expected SCSIResp, got %s", i, OpcodeName(resp.Opcode()))
|
|
}
|
|
if resp.SCSIStatus() != SCSIStatusGood {
|
|
t.Fatalf("write %d: status=%d", i, resp.SCSIStatus())
|
|
}
|
|
}
|
|
}
|
|
|
|
func testRXTXWriteWithR2T(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Write without immediate data -- should trigger R2T + Data-Out.
|
|
cmd := &PDU{}
|
|
cmd.SetOpcode(OpSCSICmd)
|
|
cmd.SetOpSpecific1(FlagF | FlagW) // no immediate data
|
|
cmd.SetInitiatorTaskTag(1)
|
|
cmd.SetExpectedDataTransferLength(4096)
|
|
cmd.SetCmdSN(0)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 10)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
cmd.SetCDB(cdb)
|
|
// No DataSegment = no immediate data
|
|
|
|
if err := WritePDU(env.clientConn, cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Expect R2T from target.
|
|
r2t, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r2t.Opcode() != OpR2T {
|
|
t.Fatalf("expected R2T, got %s", OpcodeName(r2t.Opcode()))
|
|
}
|
|
|
|
// Send Data-Out.
|
|
dataOut := &PDU{}
|
|
dataOut.SetOpcode(OpSCSIDataOut)
|
|
dataOut.SetOpSpecific1(FlagF) // Final
|
|
dataOut.SetInitiatorTaskTag(1)
|
|
dataOut.SetTargetTransferTag(r2t.TargetTransferTag())
|
|
dataOut.SetDataSN(0)
|
|
dataOut.SetBufferOffset(0)
|
|
dataOut.DataSegment = bytes.Repeat([]byte{0xCC}, 4096)
|
|
|
|
if err := WritePDU(env.clientConn, dataOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Expect SCSI Response.
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpSCSIResp {
|
|
t.Fatalf("expected SCSIResp, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
if resp.SCSIStatus() != SCSIStatusGood {
|
|
t.Fatalf("status=%d", resp.SCSIStatus())
|
|
}
|
|
}
|
|
|
|
func testRXTXMixedOps(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Interleave: WRITE, READ, NOP, READ, WRITE
|
|
cmdSN := uint32(2)
|
|
|
|
// WRITE
|
|
sendSCSIWriteImmediate(t, env.clientConn, 0, bytes.Repeat([]byte{0x11}, 4096), 1, cmdSN)
|
|
cmdSN++
|
|
resp, _ := ReadPDU(env.clientConn)
|
|
if resp.Opcode() != OpSCSIResp {
|
|
t.Fatalf("write: got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
|
|
// READ
|
|
sendSCSIRead(t, env.clientConn, 0, 1, 2, cmdSN)
|
|
cmdSN++
|
|
resp, _ = ReadPDU(env.clientConn)
|
|
if resp.Opcode() != OpSCSIDataIn {
|
|
t.Fatalf("read: got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
|
|
// NOP
|
|
nop := &PDU{}
|
|
nop.SetOpcode(OpNOPOut)
|
|
nop.SetOpSpecific1(FlagF)
|
|
nop.SetInitiatorTaskTag(3)
|
|
nop.SetImmediate(true)
|
|
WritePDU(env.clientConn, nop)
|
|
resp, _ = ReadPDU(env.clientConn)
|
|
if resp.Opcode() != OpNOPIn {
|
|
t.Fatalf("nop: got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
|
|
// TEST_UNIT_READY (CDB[0] = 0x00)
|
|
tuCmd := &PDU{}
|
|
tuCmd.SetOpcode(OpSCSICmd)
|
|
tuCmd.SetOpSpecific1(FlagF)
|
|
tuCmd.SetInitiatorTaskTag(4)
|
|
tuCmd.SetCmdSN(cmdSN)
|
|
var tuCDB [16]byte
|
|
tuCmd.SetCDB(tuCDB) // all zeros = TEST_UNIT_READY
|
|
WritePDU(env.clientConn, tuCmd)
|
|
resp, _ = ReadPDU(env.clientConn)
|
|
if resp.Opcode() != OpSCSIResp {
|
|
t.Fatalf("tur: got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
}
|
|
|
|
func testRXTXLargeReadDataIn(t *testing.T) {
|
|
env := setupSessionWithConfig(t, func(c *TargetConfig) {
|
|
c.MaxRecvDataSegmentLength = 4096 // small: force multi-PDU
|
|
})
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Read 4 blocks = 16384 bytes. With MaxRecvDataSegmentLength=4096,
|
|
// this should produce 4 Data-In PDUs.
|
|
sendSCSIRead(t, env.clientConn, 0, 4, 1, 2)
|
|
|
|
var pdus []*PDU
|
|
for {
|
|
p, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pdus = append(pdus, p)
|
|
if p.OpSpecific1()&FlagS != 0 {
|
|
break // final PDU with S-bit
|
|
}
|
|
}
|
|
|
|
if len(pdus) < 2 {
|
|
t.Fatalf("expected >= 2 Data-In PDUs, got %d", len(pdus))
|
|
}
|
|
|
|
// Reconstruct data.
|
|
totalData := make([]byte, 0)
|
|
for _, p := range pdus {
|
|
totalData = append(totalData, p.DataSegment...)
|
|
}
|
|
if len(totalData) != 4*4096 {
|
|
t.Fatalf("total data length: %d, want %d", len(totalData), 4*4096)
|
|
}
|
|
}
|
|
|
|
func testRXTXStatSNMonotonic(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
var lastStatSN uint32
|
|
cmdSN := uint32(2)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
sendSCSIRead(t, env.clientConn, 0, 1, uint32(i+1), cmdSN)
|
|
cmdSN++
|
|
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
statSN := resp.StatSN()
|
|
if i > 0 && statSN != lastStatSN+1 {
|
|
t.Fatalf("StatSN not monotonic: prev=%d, curr=%d", lastStatSN, statSN)
|
|
}
|
|
lastStatSN = statSN
|
|
}
|
|
}
|
|
|
|
func testRXTXStatSNDataInNoIncrement(t *testing.T) {
|
|
env := setupSessionWithConfig(t, func(c *TargetConfig) {
|
|
c.MaxRecvDataSegmentLength = 4096 // small: force multi-PDU
|
|
})
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Read 4 blocks to get multi-PDU Data-In.
|
|
sendSCSIRead(t, env.clientConn, 0, 4, 1, 2)
|
|
|
|
var pdus []*PDU
|
|
for {
|
|
p, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pdus = append(pdus, p)
|
|
if p.OpSpecific1()&FlagS != 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(pdus) < 2 {
|
|
t.Fatalf("expected >= 2 Data-In PDUs, got %d", len(pdus))
|
|
}
|
|
|
|
// Intermediate PDUs should NOT have StatSN incremented.
|
|
// Only the final PDU (with S-bit) should have StatSN.
|
|
finalPDU := pdus[len(pdus)-1]
|
|
if finalPDU.OpSpecific1()&FlagS == 0 {
|
|
t.Fatal("last PDU missing S-bit")
|
|
}
|
|
// StatSN on final PDU should be set.
|
|
statSN := finalPDU.StatSN()
|
|
if statSN == 0 {
|
|
// It's actually valid for StatSN to be any value (depends on login StatSN).
|
|
// Just verify it's set on the final PDU.
|
|
}
|
|
_ = statSN
|
|
}
|
|
|
|
func testRXTXStatSNMixedTypes(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
cmdSN := uint32(2)
|
|
var statSNs []uint32
|
|
|
|
// NOP
|
|
nop := &PDU{}
|
|
nop.SetOpcode(OpNOPOut)
|
|
nop.SetOpSpecific1(FlagF)
|
|
nop.SetInitiatorTaskTag(1)
|
|
nop.SetImmediate(true)
|
|
WritePDU(env.clientConn, nop)
|
|
resp, _ := ReadPDU(env.clientConn)
|
|
statSNs = append(statSNs, resp.StatSN())
|
|
|
|
// SCSI READ
|
|
sendSCSIRead(t, env.clientConn, 0, 1, 2, cmdSN)
|
|
cmdSN++
|
|
resp, _ = ReadPDU(env.clientConn)
|
|
statSNs = append(statSNs, resp.StatSN())
|
|
|
|
// Logout
|
|
logout := &PDU{}
|
|
logout.SetOpcode(OpLogoutReq)
|
|
logout.SetOpSpecific1(FlagF)
|
|
logout.SetInitiatorTaskTag(3)
|
|
logout.SetCmdSN(cmdSN)
|
|
WritePDU(env.clientConn, logout)
|
|
resp, _ = ReadPDU(env.clientConn)
|
|
statSNs = append(statSNs, resp.StatSN())
|
|
|
|
// All StatSNs should be strictly increasing.
|
|
for i := 1; i < len(statSNs); i++ {
|
|
if statSNs[i] != statSNs[i-1]+1 {
|
|
t.Errorf("StatSN not monotonic at %d: %v", i, statSNs)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testRXTXShutdownClean(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Send logout.
|
|
logout := &PDU{}
|
|
logout.SetOpcode(OpLogoutReq)
|
|
logout.SetOpSpecific1(FlagF)
|
|
logout.SetInitiatorTaskTag(1)
|
|
logout.SetCmdSN(0)
|
|
WritePDU(env.clientConn, logout)
|
|
|
|
// Read logout response.
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpLogoutResp {
|
|
t.Fatalf("expected LogoutResp, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
|
|
// HandleConnection should exit cleanly.
|
|
select {
|
|
case err := <-env.done:
|
|
if err != nil {
|
|
t.Fatalf("error after logout: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("session did not exit after logout")
|
|
}
|
|
}
|
|
|
|
func testRXTXConnDropReader(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Close client conn -- reader should detect EOF.
|
|
env.clientConn.Close()
|
|
|
|
select {
|
|
case err := <-env.done:
|
|
if err != nil {
|
|
t.Fatalf("error on conn drop: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("session did not exit after conn drop")
|
|
}
|
|
}
|
|
|
|
func testRXTXConnDropWriter(t *testing.T) {
|
|
client, server := net.Pipe()
|
|
dev := newMockDevice(1024 * 4096)
|
|
config := DefaultTargetConfig()
|
|
config.TargetName = testTargetName
|
|
resolver := newTestResolverWithDevice(dev)
|
|
logger := log.New(io.Discard, "", 0)
|
|
|
|
sess := NewSession(server, config, resolver, resolver, logger)
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- sess.HandleConnection()
|
|
}()
|
|
|
|
doLogin(t, client)
|
|
|
|
// Close the server side (writer side for txLoop).
|
|
server.Close()
|
|
|
|
// Session should exit.
|
|
select {
|
|
case <-done:
|
|
// Good -- session exited.
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("session did not exit after server-side close")
|
|
}
|
|
|
|
client.Close()
|
|
}
|
|
|
|
// testRXTXR2TStatSNFresh verifies that R2T PDUs carry a fresh StatSN
|
|
// (assigned by txLoop) rather than a stale value baked in at build time.
|
|
func testRXTXR2TStatSNFresh(t *testing.T) {
|
|
env := setupSession(t)
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Send a NOP-Out (immediate) -- its NOP-In response will consume one StatSN.
|
|
nop := &PDU{}
|
|
nop.SetOpcode(OpNOPOut)
|
|
nop.SetOpSpecific1(FlagF)
|
|
nop.SetInitiatorTaskTag(0x1000)
|
|
nop.SetImmediate(true)
|
|
if err := WritePDU(env.clientConn, nop); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Read NOP-In response.
|
|
nopResp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if nopResp.Opcode() != OpNOPIn {
|
|
t.Fatalf("expected NOP-In, got %s", OpcodeName(nopResp.Opcode()))
|
|
}
|
|
nopStatSN := nopResp.StatSN()
|
|
|
|
// Now send a WRITE command without immediate data to trigger R2T.
|
|
cmd := &PDU{}
|
|
cmd.SetOpcode(OpSCSICmd)
|
|
cmd.SetOpSpecific1(FlagF | FlagW)
|
|
cmd.SetInitiatorTaskTag(1)
|
|
cmd.SetExpectedDataTransferLength(4096)
|
|
cmd.SetCmdSN(0)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
cmd.SetCDB(cdb)
|
|
|
|
if err := WritePDU(env.clientConn, cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Read R2T -- its StatSN should be nopStatSN+1 (fresh, not stale).
|
|
r2t, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r2t.Opcode() != OpR2T {
|
|
t.Fatalf("expected R2T, got %s", OpcodeName(r2t.Opcode()))
|
|
}
|
|
|
|
// R2T uses statSNCopy: same as current StatSN (which is nopStatSN+1) without increment.
|
|
expectedStatSN := nopStatSN + 1
|
|
if r2t.StatSN() != expectedStatSN {
|
|
t.Fatalf("R2T StatSN: got %d, want %d (NOP-In was %d)", r2t.StatSN(), expectedStatSN, nopStatSN)
|
|
}
|
|
|
|
// Complete the write by sending Data-Out.
|
|
dataOut := &PDU{}
|
|
dataOut.SetOpcode(OpSCSIDataOut)
|
|
dataOut.SetOpSpecific1(FlagF)
|
|
dataOut.SetInitiatorTaskTag(1)
|
|
dataOut.SetTargetTransferTag(r2t.TargetTransferTag())
|
|
dataOut.SetDataSN(0)
|
|
dataOut.SetBufferOffset(0)
|
|
dataOut.DataSegment = bytes.Repeat([]byte{0xDD}, 4096)
|
|
|
|
if err := WritePDU(env.clientConn, dataOut); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Read SCSI response -- its StatSN should be nopStatSN+1 (R2T didn't increment).
|
|
resp, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpSCSIResp {
|
|
t.Fatalf("expected SCSIResp, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
if resp.StatSN() != expectedStatSN {
|
|
t.Fatalf("SCSI Resp StatSN: got %d, want %d", resp.StatSN(), expectedStatSN)
|
|
}
|
|
}
|
|
|
|
// testRXTXTxErrorExitsClean verifies that when txLoop encounters a write error,
|
|
// HandleConnection exits cleanly without hanging.
|
|
func testRXTXTxErrorExitsClean(t *testing.T) {
|
|
client, server := net.Pipe()
|
|
dev := newMockDevice(1024 * 4096)
|
|
config := DefaultTargetConfig()
|
|
config.TargetName = testTargetName
|
|
resolver := newTestResolverWithDevice(dev)
|
|
logger := log.New(io.Discard, "", 0)
|
|
|
|
sess := NewSession(server, config, resolver, resolver, logger)
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- sess.HandleConnection()
|
|
}()
|
|
|
|
doLogin(t, client)
|
|
|
|
// Close the server side -- this makes txLoop's WritePDU fail.
|
|
server.Close()
|
|
|
|
// Send something from client side so rxLoop dispatches and enqueues a response.
|
|
// The enqueue should not block forever thanks to txDone select.
|
|
nop := &PDU{}
|
|
nop.SetOpcode(OpNOPOut)
|
|
nop.SetOpSpecific1(FlagF)
|
|
nop.SetInitiatorTaskTag(1)
|
|
nop.SetImmediate(true)
|
|
// Write may fail since server is closed; that's fine.
|
|
WritePDU(client, nop)
|
|
|
|
// HandleConnection should exit cleanly without hanging.
|
|
select {
|
|
case <-done:
|
|
// Good.
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("HandleConnection did not exit after tx write error")
|
|
}
|
|
|
|
// Verify txDone is closed (no goroutine leak).
|
|
select {
|
|
case <-sess.txDone:
|
|
// Good -- txLoop exited.
|
|
default:
|
|
t.Fatal("txDone not closed -- txLoop may be leaked")
|
|
}
|
|
|
|
client.Close()
|
|
}
|
|
|
|
// testRXTXLoginPhaseReject verifies that handlers reject PDUs sent before
|
|
// login completes and that login can still succeed afterward.
|
|
func testRXTXLoginPhaseReject(t *testing.T) {
|
|
client, server := net.Pipe()
|
|
dev := newMockDevice(1024 * 4096)
|
|
config := DefaultTargetConfig()
|
|
config.TargetName = testTargetName
|
|
resolver := newTestResolverWithDevice(dev)
|
|
logger := log.New(io.Discard, "", 0)
|
|
|
|
sess := NewSession(server, config, resolver, resolver, logger)
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- sess.HandleConnection()
|
|
}()
|
|
|
|
defer func() {
|
|
sess.Close()
|
|
client.Close()
|
|
}()
|
|
|
|
// Send a TextReq before login -- should get a Reject (inline, not buffered).
|
|
textParams := NewParams()
|
|
textParams.Set("SendTargets", "All")
|
|
textReq := makeTextReq(textParams)
|
|
textReq.SetCmdSN(1)
|
|
|
|
if err := WritePDU(client, textReq); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := ReadPDU(client)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Opcode() != OpReject {
|
|
t.Fatalf("expected Reject, got %s", OpcodeName(resp.Opcode()))
|
|
}
|
|
if resp.BHS[2] != 0x0b {
|
|
t.Fatalf("reject reason: got 0x%02x, want 0x0b", resp.BHS[2])
|
|
}
|
|
|
|
// Login should still work after the reject.
|
|
doLogin(t, client)
|
|
|
|
// Verify session is functional by sending a NOP.
|
|
nop := &PDU{}
|
|
nop.SetOpcode(OpNOPOut)
|
|
nop.SetOpSpecific1(FlagF)
|
|
nop.SetInitiatorTaskTag(0x2222)
|
|
nop.SetImmediate(true)
|
|
if err := WritePDU(client, nop); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
nopResp, err := ReadPDU(client)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if nopResp.Opcode() != OpNOPIn {
|
|
t.Fatalf("expected NOP-In, got %s", OpcodeName(nopResp.Opcode()))
|
|
}
|
|
}
|
|
|
|
// testRXTXPendingQueueOverflow verifies that flooding non-Data-Out PDUs
|
|
// during Data-Out collection causes the session to close (not OOM).
|
|
func testRXTXPendingQueueOverflow(t *testing.T) {
|
|
env := setupSessionWithConfig(t, func(cfg *TargetConfig) {
|
|
cfg.MaxRecvDataSegmentLength = 4096
|
|
cfg.FirstBurstLength = 0 // force R2T
|
|
cfg.DataOutTimeout = 5 * time.Second
|
|
})
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Start WRITE requiring R2T.
|
|
cmd := &PDU{}
|
|
cmd.SetOpcode(OpSCSICmd)
|
|
cmd.SetOpSpecific1(FlagF | FlagW)
|
|
cmd.SetInitiatorTaskTag(0xAAAA)
|
|
cmd.SetExpectedDataTransferLength(4096)
|
|
cmd.SetCmdSN(0)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
cmd.SetCDB(cdb)
|
|
if err := WritePDU(env.clientConn, cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Read R2T.
|
|
r2t, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r2t.Opcode() != OpR2T {
|
|
t.Fatalf("expected R2T, got %s", OpcodeName(r2t.Opcode()))
|
|
}
|
|
|
|
// Flood 100 NOP-Out PDUs during Data-Out collection.
|
|
for i := 0; i < 100; i++ {
|
|
nop := &PDU{}
|
|
nop.SetOpcode(OpNOPOut)
|
|
nop.SetOpSpecific1(FlagF)
|
|
nop.SetInitiatorTaskTag(uint32(0xB000 + i))
|
|
nop.SetImmediate(true)
|
|
if err := WritePDU(env.clientConn, nop); err != nil {
|
|
break // session closed
|
|
}
|
|
}
|
|
|
|
// Session should exit with an error (pending queue overflow).
|
|
select {
|
|
case <-env.done:
|
|
// Good -- session exited.
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("session did not exit after pending overflow")
|
|
}
|
|
}
|
|
|
|
// testRXTXDataOutTimeout verifies that collectDataOut times out
|
|
// when the initiator never sends Data-Out after R2T.
|
|
func testRXTXDataOutTimeout(t *testing.T) {
|
|
env := setupSessionWithConfig(t, func(cfg *TargetConfig) {
|
|
cfg.MaxRecvDataSegmentLength = 4096
|
|
cfg.FirstBurstLength = 0 // force R2T
|
|
cfg.DataOutTimeout = 500 * time.Millisecond
|
|
})
|
|
doLogin(t, env.clientConn)
|
|
|
|
// Send WRITE command requiring R2T (no immediate data).
|
|
cmd := &PDU{}
|
|
cmd.SetOpcode(OpSCSICmd)
|
|
cmd.SetOpSpecific1(FlagF | FlagW)
|
|
cmd.SetInitiatorTaskTag(0xBEEF)
|
|
cmd.SetExpectedDataTransferLength(4096)
|
|
cmd.SetCmdSN(0)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
cmd.SetCDB(cdb)
|
|
|
|
if err := WritePDU(env.clientConn, cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Read R2T.
|
|
r2t, err := ReadPDU(env.clientConn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r2t.Opcode() != OpR2T {
|
|
t.Fatalf("expected R2T, got %s", OpcodeName(r2t.Opcode()))
|
|
}
|
|
|
|
// Do NOT send Data-Out. Session should time out and exit.
|
|
select {
|
|
case err := <-env.done:
|
|
t.Logf("session exited: %v", err)
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("session did not time out -- DataOutTimeout not working")
|
|
}
|
|
}
|