mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-19 04:24:28 +02:00
Add ALUA (Asymmetric Logical Unit Access) support to the iSCSI target, enabling dm-multipath on Linux to automatically detect path state changes and reroute I/O during HA failover without initiator-side intervention. - ALUAProvider interface with implicit ALUA (TPGS=0x01) - INQUIRY byte 5 TPGS bits, VPD 0x83 with NAA+TPG+RTP descriptors - REPORT TARGET PORT GROUPS handler (MAINTENANCE IN SA=0x0A) - MAINTENANCE OUT rejection (implicit-only, no SET TPG) - Standby write rejection (NOT_READY ASC=04h ASCQ=0Bh) - RoleNone maps to Active/Optimized (standalone single-node compatibility) - NAA-6 device identifier derived from volume UUID - -tpg-id flag with [1,65535] validation - dm-multipath config + setup script (group_by_tpg, ALUA prio) - 12 unit tests + 16 QA adversarial tests + 4 integration tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1359 lines
37 KiB
Go
1359 lines
37 KiB
Go
package iscsi
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// mockBlockDevice implements BlockDevice for testing.
|
|
type mockBlockDevice struct {
|
|
blockSize uint32
|
|
volumeSize uint64
|
|
healthy bool
|
|
blocks map[uint64][]byte // LBA -> data
|
|
syncErr error
|
|
readErr error
|
|
writeErr error
|
|
trimErr error
|
|
}
|
|
|
|
func newMockDevice(volumeSize uint64) *mockBlockDevice {
|
|
return &mockBlockDevice{
|
|
blockSize: 4096,
|
|
volumeSize: volumeSize,
|
|
healthy: true,
|
|
blocks: make(map[uint64][]byte),
|
|
}
|
|
}
|
|
|
|
func (m *mockBlockDevice) ReadAt(lba uint64, length uint32) ([]byte, error) {
|
|
if m.readErr != nil {
|
|
return nil, m.readErr
|
|
}
|
|
blockCount := length / m.blockSize
|
|
result := make([]byte, length)
|
|
for i := uint32(0); i < blockCount; i++ {
|
|
if data, ok := m.blocks[lba+uint64(i)]; ok {
|
|
copy(result[i*m.blockSize:], data)
|
|
}
|
|
// Unwritten blocks return zeros (already zeroed)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (m *mockBlockDevice) WriteAt(lba uint64, data []byte) error {
|
|
if m.writeErr != nil {
|
|
return m.writeErr
|
|
}
|
|
blockCount := uint32(len(data)) / m.blockSize
|
|
for i := uint32(0); i < blockCount; i++ {
|
|
block := make([]byte, m.blockSize)
|
|
copy(block, data[i*m.blockSize:])
|
|
m.blocks[lba+uint64(i)] = block
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBlockDevice) Trim(lba uint64, length uint32) error {
|
|
if m.trimErr != nil {
|
|
return m.trimErr
|
|
}
|
|
blockCount := length / m.blockSize
|
|
for i := uint32(0); i < blockCount; i++ {
|
|
delete(m.blocks, lba+uint64(i))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBlockDevice) SyncCache() error { return m.syncErr }
|
|
func (m *mockBlockDevice) BlockSize() uint32 { return m.blockSize }
|
|
func (m *mockBlockDevice) VolumeSize() uint64 { return m.volumeSize }
|
|
func (m *mockBlockDevice) IsHealthy() bool { return m.healthy }
|
|
|
|
func TestSCSI(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
run func(t *testing.T)
|
|
}{
|
|
{"test_unit_ready_good", testTestUnitReadyGood},
|
|
{"test_unit_ready_not_ready", testTestUnitReadyNotReady},
|
|
{"inquiry_standard", testInquiryStandard},
|
|
{"inquiry_vpd_supported_pages", testInquiryVPDSupportedPages},
|
|
{"inquiry_vpd_serial", testInquiryVPDSerial},
|
|
{"inquiry_vpd_device_id", testInquiryVPDDeviceID},
|
|
{"inquiry_vpd_unknown_page", testInquiryVPDUnknownPage},
|
|
{"inquiry_alloc_length", testInquiryAllocLength},
|
|
{"read_capacity_10", testReadCapacity10},
|
|
{"read_capacity_10_large", testReadCapacity10Large},
|
|
{"read_capacity_16", testReadCapacity16},
|
|
{"read_capacity_16_lbpme", testReadCapacity16LBPME},
|
|
{"mode_sense_6", testModeSense6},
|
|
{"report_luns", testReportLuns},
|
|
{"unknown_opcode", testUnknownOpcode},
|
|
{"read_10", testRead10},
|
|
{"read_16", testRead16},
|
|
{"write_10", testWrite10},
|
|
{"write_16", testWrite16},
|
|
{"read_write_roundtrip", testReadWriteRoundtrip},
|
|
{"write_oob", testWriteOOB},
|
|
{"read_oob", testReadOOB},
|
|
{"zero_length_transfer", testZeroLengthTransfer},
|
|
{"sync_cache", testSyncCache},
|
|
{"sync_cache_error", testSyncCacheError},
|
|
{"unmap_single", testUnmapSingle},
|
|
{"unmap_multiple_descriptors", testUnmapMultipleDescriptors},
|
|
{"unmap_short_param", testUnmapShortParam},
|
|
{"build_sense_data", testBuildSenseData},
|
|
{"read_error", testReadError},
|
|
{"write_error", testWriteError},
|
|
{"read_capacity_16_invalid_sa", testReadCapacity16InvalidSA},
|
|
{"write_same_16_unmap", testWriteSame16Unmap},
|
|
{"write_same_16_ndob", testWriteSame16NDOB},
|
|
{"write_same_16_pattern", testWriteSame16Pattern},
|
|
{"write_same_16_zeros", testWriteSame16Zeros},
|
|
{"write_same_16_oob", testWriteSame16OOB},
|
|
{"write_same_16_zero_blocks", testWriteSame16ZeroBlocks},
|
|
{"mode_sense_10", testModeSense10},
|
|
{"vpd_b0_block_limits", testVPDB0BlockLimits},
|
|
{"vpd_b2_logical_block_prov", testVPDB2LogicalBlockProv},
|
|
{"alua_inquiry_tpgs", testALUA_InquiryTPGS},
|
|
{"alua_inquiry_no_tpgs", testALUA_InquiryNoTPGS},
|
|
{"alua_vpd83_three_descriptors", testALUA_VPD83_ThreeDescriptors},
|
|
{"alua_vpd83_shared_naa", testALUA_VPD83_SharedNAA},
|
|
{"alua_report_tpg_primary", testALUA_ReportTPG_Primary},
|
|
{"alua_report_tpg_replica", testALUA_ReportTPG_Replica},
|
|
{"alua_report_tpg_stale", testALUA_ReportTPG_Stale},
|
|
{"alua_report_tpg_tpgid_matches", testALUA_ReportTPG_TPGIDMatches},
|
|
{"alua_set_tpg_rejected", testALUA_SetTPG_Rejected},
|
|
{"alua_standby_rejects_write", testALUA_StandbyRejectsWrite},
|
|
{"alua_role_none_allows_writes", testALUA_RoleNone_AllowsWrites},
|
|
{"alua_report_tpg_transitioning", testALUA_ReportTPG_Transitioning},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.run(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testTestUnitReadyGood(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiTestUnitReady
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
}
|
|
|
|
func testTestUnitReadyNotReady(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
dev.healthy = false
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiTestUnitReady
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if r.SenseKey != SenseNotReady {
|
|
t.Fatalf("sense key: %d", r.SenseKey)
|
|
}
|
|
}
|
|
|
|
func testInquiryStandard(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
binary.BigEndian.PutUint16(cdb[3:5], 96)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if len(r.Data) != 96 {
|
|
t.Fatalf("data length: %d", len(r.Data))
|
|
}
|
|
// Check peripheral device type
|
|
if r.Data[0] != 0x00 {
|
|
t.Fatal("not SBC device type")
|
|
}
|
|
// Check vendor
|
|
vendor := string(r.Data[8:16])
|
|
if vendor != "SeaweedF" {
|
|
t.Fatalf("vendor: %q", vendor)
|
|
}
|
|
// Check CmdQue
|
|
if r.Data[7]&0x02 == 0 {
|
|
t.Fatal("CmdQue not set")
|
|
}
|
|
}
|
|
|
|
func testInquiryVPDSupportedPages(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
cdb[1] = 0x01 // EVPD
|
|
cdb[2] = 0x00 // Supported pages
|
|
binary.BigEndian.PutUint16(cdb[3:5], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if r.Data[1] != 0x00 {
|
|
t.Fatal("wrong page code")
|
|
}
|
|
// Should list pages 0x00, 0x80, 0x83, 0xB0, 0xB2
|
|
want := []byte{0x00, 0x80, 0x83, 0xb0, 0xb2}
|
|
if len(r.Data) < 4+len(want) {
|
|
t.Fatalf("supported pages too short: %v", r.Data)
|
|
}
|
|
for i, p := range want {
|
|
if r.Data[4+i] != p {
|
|
t.Fatalf("page[%d]: got %02x, want %02x (data=%v)", i, r.Data[4+i], p, r.Data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testInquiryVPDSerial(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
cdb[1] = 0x01
|
|
cdb[2] = 0x80
|
|
binary.BigEndian.PutUint16(cdb[3:5], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if r.Data[1] != 0x80 {
|
|
t.Fatal("wrong page code")
|
|
}
|
|
}
|
|
|
|
func testInquiryVPDDeviceID(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
cdb[1] = 0x01
|
|
cdb[2] = 0x83
|
|
binary.BigEndian.PutUint16(cdb[3:5], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if r.Data[1] != 0x83 {
|
|
t.Fatal("wrong page code")
|
|
}
|
|
}
|
|
|
|
func testInquiryVPDUnknownPage(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
cdb[1] = 0x01
|
|
cdb[2] = 0xFF // unknown page
|
|
binary.BigEndian.PutUint16(cdb[3:5], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatal("expected CHECK_CONDITION")
|
|
}
|
|
if r.SenseKey != SenseIllegalRequest {
|
|
t.Fatal("expected ILLEGAL_REQUEST")
|
|
}
|
|
}
|
|
|
|
func testInquiryAllocLength(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
binary.BigEndian.PutUint16(cdb[3:5], 10) // small alloc
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("should succeed")
|
|
}
|
|
if len(r.Data) != 10 {
|
|
t.Fatalf("truncation: expected 10, got %d", len(r.Data))
|
|
}
|
|
}
|
|
|
|
func testReadCapacity10(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096) // 100 blocks
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiReadCapacity10
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("status not good")
|
|
}
|
|
if len(r.Data) != 8 {
|
|
t.Fatalf("data length: %d", len(r.Data))
|
|
}
|
|
lastLBA := binary.BigEndian.Uint32(r.Data[0:4])
|
|
blockSize := binary.BigEndian.Uint32(r.Data[4:8])
|
|
if lastLBA != 99 {
|
|
t.Fatalf("last LBA: %d, expected 99", lastLBA)
|
|
}
|
|
if blockSize != 4096 {
|
|
t.Fatalf("block size: %d", blockSize)
|
|
}
|
|
}
|
|
|
|
func testReadCapacity10Large(t *testing.T) {
|
|
// Volume with >2^32 blocks should return 0xFFFFFFFF
|
|
dev := newMockDevice(uint64(0x100000001) * 4096) // 2^32+1 blocks
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiReadCapacity10
|
|
r := h.HandleCommand(cdb, nil)
|
|
lastLBA := binary.BigEndian.Uint32(r.Data[0:4])
|
|
if lastLBA != 0xFFFFFFFF {
|
|
t.Fatalf("should return 0xFFFFFFFF for >2TB, got %d", lastLBA)
|
|
}
|
|
}
|
|
|
|
func testReadCapacity16(t *testing.T) {
|
|
dev := newMockDevice(3 * 1024 * 1024 * 1024 * 1024) // 3 TB
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiServiceActionIn16
|
|
cdb[1] = ScsiSAReadCapacity16
|
|
binary.BigEndian.PutUint32(cdb[10:14], 32)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("status not good")
|
|
}
|
|
lastLBA := binary.BigEndian.Uint64(r.Data[0:8])
|
|
expectedBlocks := uint64(3*1024*1024*1024*1024) / 4096
|
|
if lastLBA != expectedBlocks-1 {
|
|
t.Fatalf("last LBA: %d, expected %d", lastLBA, expectedBlocks-1)
|
|
}
|
|
}
|
|
|
|
func testReadCapacity16LBPME(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiServiceActionIn16
|
|
cdb[1] = ScsiSAReadCapacity16
|
|
binary.BigEndian.PutUint32(cdb[10:14], 32)
|
|
r := h.HandleCommand(cdb, nil)
|
|
// LBPME bit should be set (byte 14, bit 7)
|
|
if r.Data[14]&0x80 == 0 {
|
|
t.Fatal("LBPME bit not set")
|
|
}
|
|
}
|
|
|
|
func testModeSense6(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiModeSense6
|
|
cdb[4] = 255
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("status not good")
|
|
}
|
|
if len(r.Data) != 4 {
|
|
t.Fatalf("mode sense data: %d bytes", len(r.Data))
|
|
}
|
|
// No write protect
|
|
if r.Data[2]&0x80 != 0 {
|
|
t.Fatal("write protect set")
|
|
}
|
|
}
|
|
|
|
func testReportLuns(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiReportLuns
|
|
binary.BigEndian.PutUint32(cdb[6:10], 256)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("status not good")
|
|
}
|
|
lunListLen := binary.BigEndian.Uint32(r.Data[0:4])
|
|
if lunListLen != 8 {
|
|
t.Fatalf("LUN list length: %d (expected 8 for 1 LUN)", lunListLen)
|
|
}
|
|
}
|
|
|
|
func testUnknownOpcode(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = 0xFF
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatal("expected CHECK_CONDITION")
|
|
}
|
|
if r.SenseKey != SenseIllegalRequest {
|
|
t.Fatal("expected ILLEGAL_REQUEST")
|
|
}
|
|
}
|
|
|
|
func testRead10(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
// Write some data first
|
|
data := make([]byte, 4096)
|
|
for i := range data {
|
|
data[i] = 0xAB
|
|
}
|
|
dev.blocks[5] = data
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiRead10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 5) // LBA=5
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1) // 1 block
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("read failed")
|
|
}
|
|
if len(r.Data) != 4096 {
|
|
t.Fatalf("data length: %d", len(r.Data))
|
|
}
|
|
if r.Data[0] != 0xAB {
|
|
t.Fatal("data mismatch")
|
|
}
|
|
}
|
|
|
|
func testRead16(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
data := make([]byte, 4096)
|
|
data[0] = 0xCD
|
|
dev.blocks[10] = data
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiRead16
|
|
binary.BigEndian.PutUint64(cdb[2:10], 10)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 1)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("read16 failed")
|
|
}
|
|
if r.Data[0] != 0xCD {
|
|
t.Fatal("data mismatch")
|
|
}
|
|
}
|
|
|
|
func testWrite10(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
dataOut := make([]byte, 4096)
|
|
dataOut[0] = 0xEF
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 7)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
r := h.HandleCommand(cdb, dataOut)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("write failed")
|
|
}
|
|
if dev.blocks[7][0] != 0xEF {
|
|
t.Fatal("data not written")
|
|
}
|
|
}
|
|
|
|
func testWrite16(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
dataOut := make([]byte, 8192)
|
|
dataOut[0] = 0x11
|
|
dataOut[4096] = 0x22
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite16
|
|
binary.BigEndian.PutUint64(cdb[2:10], 50)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 2)
|
|
r := h.HandleCommand(cdb, dataOut)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("write16 failed")
|
|
}
|
|
if dev.blocks[50][0] != 0x11 {
|
|
t.Fatal("block 50 wrong")
|
|
}
|
|
if dev.blocks[51][0] != 0x22 {
|
|
t.Fatal("block 51 wrong")
|
|
}
|
|
}
|
|
|
|
func testReadWriteRoundtrip(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
// Write
|
|
dataOut := make([]byte, 4096)
|
|
for i := range dataOut {
|
|
dataOut[i] = byte(i % 256)
|
|
}
|
|
var wcdb [16]byte
|
|
wcdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(wcdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(wcdb[7:9], 1)
|
|
h.HandleCommand(wcdb, dataOut)
|
|
|
|
// Read back
|
|
var rcdb [16]byte
|
|
rcdb[0] = ScsiRead10
|
|
binary.BigEndian.PutUint32(rcdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(rcdb[7:9], 1)
|
|
r := h.HandleCommand(rcdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("read failed")
|
|
}
|
|
for i := 0; i < 4096; i++ {
|
|
if r.Data[i] != byte(i%256) {
|
|
t.Fatalf("byte %d: got %d, want %d", i, r.Data[i], i%256)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testWriteOOB(t *testing.T) {
|
|
dev := newMockDevice(10 * 4096) // 10 blocks
|
|
h := NewSCSIHandler(dev)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 9)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 2) // LBA 9 + 2 blocks > 10
|
|
r := h.HandleCommand(cdb, make([]byte, 8192))
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatal("should fail for OOB")
|
|
}
|
|
}
|
|
|
|
func testReadOOB(t *testing.T) {
|
|
dev := newMockDevice(10 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiRead10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 10) // LBA 10 == total blocks
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatal("should fail for OOB")
|
|
}
|
|
}
|
|
|
|
func testZeroLengthTransfer(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiRead10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 0) // 0 blocks
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("zero-length read should succeed")
|
|
}
|
|
}
|
|
|
|
func testSyncCache(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiSyncCache10
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("sync cache failed")
|
|
}
|
|
}
|
|
|
|
func testSyncCacheError(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
dev.syncErr = errors.New("disk error")
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiSyncCache10
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatal("should fail")
|
|
}
|
|
}
|
|
|
|
func testUnmapSingle(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
// Write data at LBA 5
|
|
dev.blocks[5] = make([]byte, 4096)
|
|
dev.blocks[5][0] = 0xFF
|
|
|
|
// UNMAP parameter list
|
|
unmapData := make([]byte, 24) // 8 header + 16 descriptor
|
|
binary.BigEndian.PutUint16(unmapData[0:2], 22) // data length
|
|
binary.BigEndian.PutUint16(unmapData[2:4], 16) // block desc length
|
|
binary.BigEndian.PutUint64(unmapData[8:16], 5) // LBA
|
|
binary.BigEndian.PutUint32(unmapData[16:20], 1) // num blocks
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiUnmap
|
|
r := h.HandleCommand(cdb, unmapData)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("unmap failed")
|
|
}
|
|
if _, ok := dev.blocks[5]; ok {
|
|
t.Fatal("block 5 should be trimmed")
|
|
}
|
|
}
|
|
|
|
func testUnmapMultipleDescriptors(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
dev.blocks[3] = make([]byte, 4096)
|
|
dev.blocks[7] = make([]byte, 4096)
|
|
|
|
// 2 descriptors
|
|
unmapData := make([]byte, 40) // 8 header + 2*16 descriptors
|
|
binary.BigEndian.PutUint16(unmapData[0:2], 38)
|
|
binary.BigEndian.PutUint16(unmapData[2:4], 32)
|
|
// Descriptor 1: LBA=3, count=1
|
|
binary.BigEndian.PutUint64(unmapData[8:16], 3)
|
|
binary.BigEndian.PutUint32(unmapData[16:20], 1)
|
|
// Descriptor 2: LBA=7, count=1
|
|
binary.BigEndian.PutUint64(unmapData[24:32], 7)
|
|
binary.BigEndian.PutUint32(unmapData[32:36], 1)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiUnmap
|
|
r := h.HandleCommand(cdb, unmapData)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("unmap failed")
|
|
}
|
|
if _, ok := dev.blocks[3]; ok {
|
|
t.Fatal("block 3 should be trimmed")
|
|
}
|
|
if _, ok := dev.blocks[7]; ok {
|
|
t.Fatal("block 7 should be trimmed")
|
|
}
|
|
}
|
|
|
|
func testUnmapShortParam(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiUnmap
|
|
r := h.HandleCommand(cdb, []byte{1, 2, 3}) // too short
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatal("should fail for short unmap params")
|
|
}
|
|
}
|
|
|
|
func testBuildSenseData(t *testing.T) {
|
|
data := BuildSenseData(SenseIllegalRequest, ASCInvalidOpcode, ASCQLuk)
|
|
if len(data) != 18 {
|
|
t.Fatalf("length: %d", len(data))
|
|
}
|
|
if data[0] != 0x70 {
|
|
t.Fatal("response code wrong")
|
|
}
|
|
if data[2] != SenseIllegalRequest {
|
|
t.Fatal("sense key wrong")
|
|
}
|
|
if data[12] != ASCInvalidOpcode {
|
|
t.Fatal("ASC wrong")
|
|
}
|
|
}
|
|
|
|
func testReadError(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
dev.readErr = errors.New("io error")
|
|
h := NewSCSIHandler(dev)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiRead10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatal("should fail")
|
|
}
|
|
if r.SenseKey != SenseMediumError {
|
|
t.Fatal("should be MEDIUM_ERROR")
|
|
}
|
|
}
|
|
|
|
func testWriteError(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
dev.writeErr = errors.New("io error")
|
|
h := NewSCSIHandler(dev)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
r := h.HandleCommand(cdb, make([]byte, 4096))
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatal("should fail")
|
|
}
|
|
}
|
|
|
|
// --- WRITE SAME(16) tests ---
|
|
|
|
func testWriteSame16Unmap(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
// Write data at LBA 10-14
|
|
for i := uint64(10); i < 15; i++ {
|
|
block := make([]byte, 4096)
|
|
block[0] = byte(i)
|
|
dev.blocks[i] = block
|
|
}
|
|
|
|
// WRITE SAME(16) with UNMAP bit: should trim LBA 10-14
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWriteSame16
|
|
cdb[1] = 0x08 // UNMAP bit
|
|
binary.BigEndian.PutUint64(cdb[2:10], 10)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 5)
|
|
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
for i := uint64(10); i < 15; i++ {
|
|
if _, ok := dev.blocks[i]; ok {
|
|
t.Fatalf("block %d should be trimmed", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testWriteSame16NDOB(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
dev.blocks[20] = make([]byte, 4096)
|
|
dev.blocks[20][0] = 0xFF
|
|
|
|
// WRITE SAME(16) with NDOB bit (no data-out buffer): zero the range
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWriteSame16
|
|
cdb[1] = 0x01 // NDOB bit
|
|
binary.BigEndian.PutUint64(cdb[2:10], 20)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 1)
|
|
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if _, ok := dev.blocks[20]; ok {
|
|
t.Fatal("block 20 should be trimmed")
|
|
}
|
|
}
|
|
|
|
func testWriteSame16Pattern(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
// Write a non-zero pattern across 3 blocks
|
|
pattern := make([]byte, 4096)
|
|
pattern[0] = 0xAB
|
|
pattern[4095] = 0xCD
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWriteSame16
|
|
// No UNMAP, no NDOB -- normal write same
|
|
binary.BigEndian.PutUint64(cdb[2:10], 30)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 3)
|
|
|
|
r := h.HandleCommand(cdb, pattern)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
for i := uint64(30); i < 33; i++ {
|
|
if dev.blocks[i][0] != 0xAB {
|
|
t.Fatalf("block %d[0]: got %02x, want AB", i, dev.blocks[i][0])
|
|
}
|
|
if dev.blocks[i][4095] != 0xCD {
|
|
t.Fatalf("block %d[4095]: got %02x, want CD", i, dev.blocks[i][4095])
|
|
}
|
|
}
|
|
}
|
|
|
|
func testWriteSame16Zeros(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
// Write data at LBA 40
|
|
dev.blocks[40] = make([]byte, 4096)
|
|
dev.blocks[40][0] = 0xFF
|
|
|
|
// WRITE SAME with all-zero pattern -- should use Trim for efficiency
|
|
pattern := make([]byte, 4096)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWriteSame16
|
|
binary.BigEndian.PutUint64(cdb[2:10], 40)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 1)
|
|
|
|
r := h.HandleCommand(cdb, pattern)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if _, ok := dev.blocks[40]; ok {
|
|
t.Fatal("block 40 should be trimmed (zero pattern)")
|
|
}
|
|
}
|
|
|
|
func testWriteSame16OOB(t *testing.T) {
|
|
dev := newMockDevice(10 * 4096) // 10 blocks
|
|
h := NewSCSIHandler(dev)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWriteSame16
|
|
cdb[1] = 0x08 // UNMAP
|
|
binary.BigEndian.PutUint64(cdb[2:10], 8)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 5) // 8+5 > 10
|
|
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatal("should fail for OOB")
|
|
}
|
|
if r.SenseASC != ASCLBAOutOfRange {
|
|
t.Fatalf("expected LBA_OUT_OF_RANGE, got ASC=%02x", r.SenseASC)
|
|
}
|
|
}
|
|
|
|
func testWriteSame16ZeroBlocks(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWriteSame16
|
|
binary.BigEndian.PutUint64(cdb[2:10], 0)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 0) // 0 blocks = no-op
|
|
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatal("zero-block write same should succeed")
|
|
}
|
|
}
|
|
|
|
func testModeSense10(t *testing.T) {
|
|
dev := newMockDevice(1024 * 1024)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiModeSense10
|
|
binary.BigEndian.PutUint16(cdb[7:9], 255) // alloc length
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if len(r.Data) != 8 {
|
|
t.Fatalf("mode sense(10) data: %d bytes, want 8", len(r.Data))
|
|
}
|
|
// Mode data length field (bytes 0-1) = 6
|
|
mdl := binary.BigEndian.Uint16(r.Data[0:2])
|
|
if mdl != 6 {
|
|
t.Fatalf("mode data length: %d, want 6", mdl)
|
|
}
|
|
// No write protect (byte 3, bit 7)
|
|
if r.Data[3]&0x80 != 0 {
|
|
t.Fatal("write protect set")
|
|
}
|
|
// Block descriptor length = 0 (bytes 6-7)
|
|
bdl := binary.BigEndian.Uint16(r.Data[6:8])
|
|
if bdl != 0 {
|
|
t.Fatalf("block descriptor length: %d, want 0", bdl)
|
|
}
|
|
}
|
|
|
|
func testVPDB0BlockLimits(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096) // 100 blocks
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
cdb[1] = 0x01 // EVPD
|
|
cdb[2] = 0xb0 // Block Limits
|
|
binary.BigEndian.PutUint16(cdb[3:5], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if r.Data[1] != 0xb0 {
|
|
t.Fatalf("page code: %02x, want B0", r.Data[1])
|
|
}
|
|
// Page length (bytes 2-3) = 60
|
|
pgLen := binary.BigEndian.Uint16(r.Data[2:4])
|
|
if pgLen != 60 {
|
|
t.Fatalf("page length: %d, want 60", pgLen)
|
|
}
|
|
// Max WRITE SAME length (bytes 40-47) should be 100 (total blocks)
|
|
maxWS := binary.BigEndian.Uint64(r.Data[40:48])
|
|
if maxWS != 100 {
|
|
t.Fatalf("max WRITE SAME length: %d, want 100", maxWS)
|
|
}
|
|
// Max UNMAP LBA count (bytes 24-27) should be 100
|
|
maxUnmap := binary.BigEndian.Uint32(r.Data[24:28])
|
|
if maxUnmap != 100 {
|
|
t.Fatalf("max UNMAP LBA count: %d, want 100", maxUnmap)
|
|
}
|
|
}
|
|
|
|
func testVPDB2LogicalBlockProv(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
cdb[1] = 0x01 // EVPD
|
|
cdb[2] = 0xb2 // Logical Block Provisioning
|
|
binary.BigEndian.PutUint16(cdb[3:5], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if r.Data[1] != 0xb2 {
|
|
t.Fatalf("page code: %02x, want B2", r.Data[1])
|
|
}
|
|
// Provisioning type (byte 5, bits 2:0) = 0x02 (thin)
|
|
if r.Data[5]&0x07 != 0x02 {
|
|
t.Fatalf("provisioning type: %02x, want 02", r.Data[5]&0x07)
|
|
}
|
|
// LBPU=1 (byte 6, bit 7), LBPWS=1 (byte 6, bit 6)
|
|
if r.Data[6]&0xC0 != 0xC0 {
|
|
t.Fatalf("LBPU/LBPWS flags: %02x, want C0", r.Data[6]&0xC0)
|
|
}
|
|
}
|
|
|
|
func testReadCapacity16InvalidSA(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiServiceActionIn16
|
|
cdb[1] = 0x05 // wrong service action
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatal("should fail for wrong SA")
|
|
}
|
|
}
|
|
|
|
// --- ALUA tests ---
|
|
|
|
// mockALUADevice embeds mockBlockDevice and adds ALUAProvider support.
|
|
type mockALUADevice struct {
|
|
*mockBlockDevice
|
|
aluaState uint8
|
|
tpgID uint16
|
|
naa [8]byte
|
|
}
|
|
|
|
func newMockALUADevice(volumeSize uint64, state uint8, tpgID uint16) *mockALUADevice {
|
|
return &mockALUADevice{
|
|
mockBlockDevice: newMockDevice(volumeSize),
|
|
aluaState: state,
|
|
tpgID: tpgID,
|
|
naa: [8]byte{0x60, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x01},
|
|
}
|
|
}
|
|
|
|
func (m *mockALUADevice) ALUAState() uint8 { return m.aluaState }
|
|
func (m *mockALUADevice) TPGroupID() uint16 { return m.tpgID }
|
|
func (m *mockALUADevice) DeviceNAA() [8]byte { return m.naa }
|
|
|
|
// testALUA_InquiryTPGS verifies byte 5 has TPGS=01 when ALUAProvider is present.
|
|
func testALUA_InquiryTPGS(t *testing.T) {
|
|
dev := newMockALUADevice(100*4096, ALUAActiveOptimized, 1)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
binary.BigEndian.PutUint16(cdb[3:5], 96)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
// Byte 5 bits 5:4 should be 01 (implicit ALUA) = 0x10
|
|
if r.Data[5]&0x30 != 0x10 {
|
|
t.Fatalf("TPGS bits: got %02x, want 0x10 in byte 5 (%02x)", r.Data[5]&0x30, r.Data[5])
|
|
}
|
|
}
|
|
|
|
// testALUA_InquiryNoTPGS verifies byte 5 has TPGS=00 when no ALUAProvider.
|
|
func testALUA_InquiryNoTPGS(t *testing.T) {
|
|
dev := newMockDevice(100 * 4096)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
binary.BigEndian.PutUint16(cdb[3:5], 96)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
// Byte 5 bits 5:4 should be 00 (no ALUA)
|
|
if r.Data[5]&0x30 != 0x00 {
|
|
t.Fatalf("TPGS bits: got %02x, want 0x00 in byte 5 (%02x)", r.Data[5]&0x30, r.Data[5])
|
|
}
|
|
}
|
|
|
|
// testALUA_VPD83_ThreeDescriptors verifies VPD 0x83 has NAA + TPG + RTP descriptors.
|
|
func testALUA_VPD83_ThreeDescriptors(t *testing.T) {
|
|
dev := newMockALUADevice(100*4096, ALUAActiveOptimized, 42)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
cdb[1] = 0x01 // EVPD
|
|
cdb[2] = 0x83
|
|
binary.BigEndian.PutUint16(cdb[3:5], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
|
|
// Page length is at bytes 2-3
|
|
pageLen := binary.BigEndian.Uint16(r.Data[2:4])
|
|
// 12 (NAA) + 8 (TPG) + 8 (RTP) = 28
|
|
if pageLen != 28 {
|
|
t.Fatalf("page length: %d, want 28 (NAA=12 + TPG=8 + RTP=8)", pageLen)
|
|
}
|
|
|
|
// Descriptor 1 (offset 4): NAA type=3
|
|
if r.Data[4+1]&0x0F != 0x03 {
|
|
t.Fatalf("desc 1 type: %02x, want 03 (NAA)", r.Data[4+1]&0x0F)
|
|
}
|
|
// Descriptor 2 (offset 4+12=16): TPG type=5
|
|
if r.Data[16+1]&0x0F != 0x05 {
|
|
t.Fatalf("desc 2 type: %02x, want 05 (TPG)", r.Data[16+1]&0x0F)
|
|
}
|
|
// TPG ID should be 42
|
|
tpgID := binary.BigEndian.Uint16(r.Data[16+6 : 16+8])
|
|
if tpgID != 42 {
|
|
t.Fatalf("TPG ID: %d, want 42", tpgID)
|
|
}
|
|
// Descriptor 3 (offset 16+8=24): RTP type=4
|
|
if r.Data[24+1]&0x0F != 0x04 {
|
|
t.Fatalf("desc 3 type: %02x, want 04 (RTP)", r.Data[24+1]&0x0F)
|
|
}
|
|
// Relative target port = 1
|
|
rtp := binary.BigEndian.Uint16(r.Data[24+6 : 24+8])
|
|
if rtp != 1 {
|
|
t.Fatalf("RTP: %d, want 1", rtp)
|
|
}
|
|
}
|
|
|
|
// testALUA_VPD83_SharedNAA verifies NAA is derived from UUID (deterministic).
|
|
func testALUA_VPD83_SharedNAA(t *testing.T) {
|
|
dev := newMockALUADevice(100*4096, ALUAActiveOptimized, 1)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiInquiry
|
|
cdb[1] = 0x01
|
|
cdb[2] = 0x83
|
|
binary.BigEndian.PutUint16(cdb[3:5], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
|
|
// NAA identifier starts at offset 4+4=8 (after page header + desc header)
|
|
naa := r.Data[8:16]
|
|
want := dev.naa[:]
|
|
for i := range want {
|
|
if naa[i] != want[i] {
|
|
t.Fatalf("NAA byte %d: got %02x, want %02x", i, naa[i], want[i])
|
|
}
|
|
}
|
|
|
|
// Run again -- same result (deterministic)
|
|
r2 := h.HandleCommand(cdb, nil)
|
|
naa2 := r2.Data[8:16]
|
|
for i := range naa {
|
|
if naa[i] != naa2[i] {
|
|
t.Fatalf("NAA not deterministic at byte %d: %02x vs %02x", i, naa[i], naa2[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// testALUA_ReportTPG_Primary verifies REPORT TARGET PORT GROUPS returns Active/Optimized.
|
|
func testALUA_ReportTPG_Primary(t *testing.T) {
|
|
dev := newMockALUADevice(100*4096, ALUAActiveOptimized, 1)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiMaintenanceIn
|
|
cdb[1] = 0x0a // SA = REPORT TARGET PORT GROUPS
|
|
binary.BigEndian.PutUint32(cdb[6:10], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
// Return data length (bytes 0-3) should be 12
|
|
rdl := binary.BigEndian.Uint32(r.Data[0:4])
|
|
if rdl != 12 {
|
|
t.Fatalf("return data length: %d, want 12", rdl)
|
|
}
|
|
// ALUA state is in byte 4 (lower nibble)
|
|
state := r.Data[4] & 0x0F
|
|
if state != ALUAActiveOptimized {
|
|
t.Fatalf("ALUA state: %02x, want %02x (Active/Optimized)", state, ALUAActiveOptimized)
|
|
}
|
|
}
|
|
|
|
// testALUA_ReportTPG_Replica verifies REPORT TARGET PORT GROUPS returns Standby.
|
|
func testALUA_ReportTPG_Replica(t *testing.T) {
|
|
dev := newMockALUADevice(100*4096, ALUAStandby, 2)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiMaintenanceIn
|
|
cdb[1] = 0x0a
|
|
binary.BigEndian.PutUint32(cdb[6:10], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
state := r.Data[4] & 0x0F
|
|
if state != ALUAStandby {
|
|
t.Fatalf("ALUA state: %02x, want %02x (Standby)", state, ALUAStandby)
|
|
}
|
|
}
|
|
|
|
// testALUA_ReportTPG_Stale verifies REPORT TARGET PORT GROUPS returns Unavailable.
|
|
func testALUA_ReportTPG_Stale(t *testing.T) {
|
|
dev := newMockALUADevice(100*4096, ALUAUnavailable, 1)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiMaintenanceIn
|
|
cdb[1] = 0x0a
|
|
binary.BigEndian.PutUint32(cdb[6:10], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
state := r.Data[4] & 0x0F
|
|
if state != ALUAUnavailable {
|
|
t.Fatalf("ALUA state: %02x, want %02x (Unavailable)", state, ALUAUnavailable)
|
|
}
|
|
}
|
|
|
|
// testALUA_ReportTPG_TPGIDMatches verifies TPG ID in response matches configured value.
|
|
func testALUA_ReportTPG_TPGIDMatches(t *testing.T) {
|
|
dev := newMockALUADevice(100*4096, ALUAActiveOptimized, 0x1234)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiMaintenanceIn
|
|
cdb[1] = 0x0a
|
|
binary.BigEndian.PutUint32(cdb[6:10], 255)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
// TPG ID at bytes 8-9
|
|
tpgID := binary.BigEndian.Uint16(r.Data[8:10])
|
|
if tpgID != 0x1234 {
|
|
t.Fatalf("TPG ID: %04x, want 1234", tpgID)
|
|
}
|
|
}
|
|
|
|
// testALUA_SetTPG_Rejected verifies MAINTENANCE OUT returns ILLEGAL_REQUEST.
|
|
func testALUA_SetTPG_Rejected(t *testing.T) {
|
|
dev := newMockALUADevice(100*4096, ALUAActiveOptimized, 1)
|
|
h := NewSCSIHandler(dev)
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiMaintenanceOut
|
|
cdb[1] = 0x0a // SA = SET TARGET PORT GROUPS
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatalf("expected CHECK_CONDITION, got %d", r.Status)
|
|
}
|
|
if r.SenseKey != SenseIllegalRequest {
|
|
t.Fatalf("expected ILLEGAL_REQUEST, got %02x", r.SenseKey)
|
|
}
|
|
}
|
|
|
|
// testALUA_StandbyRejectsWrite verifies Write on Standby returns NOT_READY.
|
|
func testALUA_StandbyRejectsWrite(t *testing.T) {
|
|
dev := newMockALUADevice(100*4096, ALUAStandby, 1)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
// Test Write10
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
r := h.HandleCommand(cdb, make([]byte, 4096))
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatalf("Write10: expected CHECK_CONDITION, got %d", r.Status)
|
|
}
|
|
if r.SenseKey != SenseNotReady {
|
|
t.Fatalf("Write10: expected NOT_READY (0x02), got %02x", r.SenseKey)
|
|
}
|
|
if r.SenseASC != 0x04 || r.SenseASCQ != 0x0B {
|
|
t.Fatalf("Write10: expected ASC=04 ASCQ=0B, got ASC=%02x ASCQ=%02x", r.SenseASC, r.SenseASCQ)
|
|
}
|
|
|
|
// Test Write16
|
|
cdb = [16]byte{}
|
|
cdb[0] = ScsiWrite16
|
|
binary.BigEndian.PutUint64(cdb[2:10], 0)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 1)
|
|
r = h.HandleCommand(cdb, make([]byte, 4096))
|
|
if r.Status != SCSIStatusCheckCond {
|
|
t.Fatalf("Write16: expected CHECK_CONDITION, got %d", r.Status)
|
|
}
|
|
if r.SenseKey != SenseNotReady {
|
|
t.Fatalf("Write16: expected NOT_READY, got %02x", r.SenseKey)
|
|
}
|
|
|
|
// Test WriteSame16
|
|
cdb = [16]byte{}
|
|
cdb[0] = ScsiWriteSame16
|
|
cdb[1] = 0x08 // UNMAP
|
|
binary.BigEndian.PutUint64(cdb[2:10], 0)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 1)
|
|
r = h.HandleCommand(cdb, nil)
|
|
if r.SenseKey != SenseNotReady {
|
|
t.Fatalf("WriteSame16: expected NOT_READY, got %02x", r.SenseKey)
|
|
}
|
|
|
|
// Test Unmap
|
|
unmapData := make([]byte, 24)
|
|
binary.BigEndian.PutUint16(unmapData[0:2], 22)
|
|
binary.BigEndian.PutUint16(unmapData[2:4], 16)
|
|
binary.BigEndian.PutUint64(unmapData[8:16], 0)
|
|
binary.BigEndian.PutUint32(unmapData[16:20], 1)
|
|
cdb = [16]byte{}
|
|
cdb[0] = ScsiUnmap
|
|
r = h.HandleCommand(cdb, unmapData)
|
|
if r.SenseKey != SenseNotReady {
|
|
t.Fatalf("Unmap: expected NOT_READY, got %02x", r.SenseKey)
|
|
}
|
|
|
|
// Test SyncCache10
|
|
cdb = [16]byte{}
|
|
cdb[0] = ScsiSyncCache10
|
|
r = h.HandleCommand(cdb, nil)
|
|
if r.SenseKey != SenseNotReady {
|
|
t.Fatalf("SyncCache10: expected NOT_READY, got %02x", r.SenseKey)
|
|
}
|
|
|
|
// Test SyncCache16
|
|
cdb = [16]byte{}
|
|
cdb[0] = ScsiSyncCache16
|
|
r = h.HandleCommand(cdb, nil)
|
|
if r.SenseKey != SenseNotReady {
|
|
t.Fatalf("SyncCache16: expected NOT_READY, got %02x", r.SenseKey)
|
|
}
|
|
|
|
// Verify reads still work on Standby
|
|
cdb = [16]byte{}
|
|
cdb[0] = ScsiRead10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
r = h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("Read10 should succeed on Standby, got status %d", r.Status)
|
|
}
|
|
}
|
|
|
|
// testALUA_RoleNone_AllowsWrites verifies that Active/Optimized state (which
|
|
// RoleNone maps to in the adapter) allows writes through checkStandbyReject.
|
|
// This covers the Fix-1 regression: standalone single-node targets must accept
|
|
// writes even without an explicit role assignment from a master.
|
|
func testALUA_RoleNone_AllowsWrites(t *testing.T) {
|
|
// Active/Optimized = what RoleNone maps to after Fix 1
|
|
dev := newMockALUADevice(100*4096, ALUAActiveOptimized, 1)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
// Write10 should succeed
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiWrite10
|
|
binary.BigEndian.PutUint32(cdb[2:6], 0)
|
|
binary.BigEndian.PutUint16(cdb[7:9], 1)
|
|
r := h.HandleCommand(cdb, make([]byte, 4096))
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("Write10: expected GOOD, got status %d (sense=%02x ASC=%02x ASCQ=%02x)",
|
|
r.Status, r.SenseKey, r.SenseASC, r.SenseASCQ)
|
|
}
|
|
|
|
// Write16 should succeed
|
|
cdb = [16]byte{}
|
|
cdb[0] = ScsiWrite16
|
|
binary.BigEndian.PutUint64(cdb[2:10], 0)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 1)
|
|
r = h.HandleCommand(cdb, make([]byte, 4096))
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("Write16: expected GOOD, got status %d", r.Status)
|
|
}
|
|
|
|
// SyncCache10 should succeed
|
|
cdb = [16]byte{}
|
|
cdb[0] = ScsiSyncCache10
|
|
r = h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("SyncCache10: expected GOOD, got status %d", r.Status)
|
|
}
|
|
|
|
// WriteSame16 (UNMAP) should succeed
|
|
cdb = [16]byte{}
|
|
cdb[0] = ScsiWriteSame16
|
|
cdb[1] = 0x08 // UNMAP
|
|
binary.BigEndian.PutUint64(cdb[2:10], 0)
|
|
binary.BigEndian.PutUint32(cdb[10:14], 1)
|
|
r = h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("WriteSame16 UNMAP: expected GOOD, got status %d", r.Status)
|
|
}
|
|
}
|
|
|
|
// testALUA_ReportTPG_Transitioning verifies REPORT TARGET PORT GROUPS returns
|
|
// state=Transitioning (0x0F) and supported flags include T_SUP (bit 3).
|
|
// This covers Fix-2: we use ALUATransitioning for RoleDraining/RoleRebuilding,
|
|
// so T_SUP must be advertised.
|
|
func testALUA_ReportTPG_Transitioning(t *testing.T) {
|
|
dev := newMockALUADevice(100*4096, ALUATransitioning, 5)
|
|
h := NewSCSIHandler(dev)
|
|
|
|
var cdb [16]byte
|
|
cdb[0] = ScsiMaintenanceIn
|
|
cdb[1] = 0x0A // SA = REPORT TARGET PORT GROUPS
|
|
binary.BigEndian.PutUint32(cdb[6:10], 256)
|
|
r := h.HandleCommand(cdb, nil)
|
|
if r.Status != SCSIStatusGood {
|
|
t.Fatalf("status: %d", r.Status)
|
|
}
|
|
if len(r.Data) < 16 {
|
|
t.Fatalf("data too short: %d", len(r.Data))
|
|
}
|
|
|
|
// Byte 4: asymmetric access state (lower 4 bits) = 0x0F (Transitioning)
|
|
gotState := r.Data[4] & 0x0F
|
|
if gotState != ALUATransitioning {
|
|
t.Fatalf("state: got %02x, want %02x (Transitioning)", gotState, ALUATransitioning)
|
|
}
|
|
|
|
// Byte 5: supported states flags -- T_SUP (bit 3) must be set
|
|
supFlags := r.Data[5]
|
|
if supFlags&0x08 == 0 {
|
|
t.Fatalf("T_SUP (bit 3) not set in supported flags: %02x", supFlags)
|
|
}
|
|
// Also verify O_SUP (bit 0), S_SUP (bit 1), U_SUP (bit 2) are set
|
|
if supFlags&0x07 != 0x07 {
|
|
t.Fatalf("O_SUP|S_SUP|U_SUP missing: got %02x, want bits 0-2 set", supFlags)
|
|
}
|
|
// Full value should be 0x0F
|
|
if supFlags != 0x0F {
|
|
t.Fatalf("supported flags: got %02x, want 0x0F", supFlags)
|
|
}
|
|
|
|
// TPG ID should be 5
|
|
tpgID := binary.BigEndian.Uint16(r.Data[8:10])
|
|
if tpgID != 5 {
|
|
t.Fatalf("TPG ID: got %d, want 5", tpgID)
|
|
}
|
|
}
|