Files
seaweedfs/weed/storage/blockvol/iscsi/scsi.go
T
Ping QiuandClaude Opus 4.6 da1b81d1c9 feat: CP8-3-1 durability modes + testrunner platform + 21 adversarial tests
Durability mode implementation (sync_all, sync_quorum, best_effort):
- DurabilityMode type with superblock persistence, parse/validate/string
- MakeDistributedSync mode-aware barrier enforcement in dist_group_commit
- blockerr sentinel package (ErrDurabilityBarrierFailed, ErrDurabilityQuorumLost)
- gRPC create path: mode validation, idempotent create consistency, partial cleanup
- F1: strict mode rejects partial replica provisioning with cleanup
- F3: empty heartbeat does not overwrite persisted strict mode
- F4: SCSI error mapping uses errors.Is sentinels (not string matching)
- Proto/wire/blockapi/CLI/UI plumbing for durability_mode field
- Observability dashboard: cluster health cards + per-volume columns

Testrunner platform (YAML-driven integration test framework):
- Engine, parser, registry, reporter (JUnit XML + HTML), metrics scraping
- 52 registered actions: block, iSCSI, I/O, fault injection, assertions
- Baseline regression framework with 7 hard-fail conditions
- 15 YAML scenarios (smoke, crash, HA, fault, consistency, snapshot)
- 49 unit tests for testrunner internals

QA adversarial suite (21 tests, all PASS):
- Idempotent create mode/RF mismatch detection
- Heartbeat mode downgrade prevention (F3)
- sync_all/sync_quorum partial replica enforcement (F1)
- Concurrent create race safety
- Failover/expand mode preservation
- Cleanup resilience when delete fails
- Master restart auto-register mode handling
- Superblock roundtrip all 3 modes
- Validate edge cases (mode×RF matrix)
- RequiredReplicas quorum math verification
- Sentinel error categorization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:06:51 -08:00

858 lines
25 KiB
Go

package iscsi
import (
"encoding/binary"
"errors"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/blockerr"
)
// SCSI opcode constants (SPC-5 / SBC-4)
const (
ScsiTestUnitReady uint8 = 0x00
ScsiRequestSense uint8 = 0x03
ScsiInquiry uint8 = 0x12
ScsiModeSelect6 uint8 = 0x15
ScsiModeSense6 uint8 = 0x1a
ScsiStartStopUnit uint8 = 0x1b
ScsiReadCapacity10 uint8 = 0x25
ScsiRead10 uint8 = 0x28
ScsiWrite10 uint8 = 0x2a
ScsiSyncCache10 uint8 = 0x35
ScsiUnmap uint8 = 0x42
ScsiModeSelect10 uint8 = 0x55
ScsiModeSense10 uint8 = 0x5a
ScsiPersistReserveIn uint8 = 0x5e
ScsiPersistReserveOut uint8 = 0x5f
ScsiRead16 uint8 = 0x88
ScsiWrite16 uint8 = 0x8a
ScsiSyncCache16 uint8 = 0x91
ScsiWriteSame16 uint8 = 0x93
ScsiServiceActionIn16 uint8 = 0x9e // READ CAPACITY(16), etc.
ScsiReportLuns uint8 = 0xa0
ScsiMaintenanceIn uint8 = 0xa3 // REPORT SUPPORTED OPCODES, REPORT TARGET PORT GROUPS, etc.
ScsiMaintenanceOut uint8 = 0xa4 // SET TARGET PORT GROUPS, etc.
)
// Service action for READ CAPACITY (16)
const ScsiSAReadCapacity16 uint8 = 0x10
// SCSI sense keys
const (
SenseNoSense uint8 = 0x00
SenseNotReady uint8 = 0x02
SenseMediumError uint8 = 0x03
SenseHardwareError uint8 = 0x04
SenseIllegalRequest uint8 = 0x05
SenseAbortedCommand uint8 = 0x0b
)
// ASC/ASCQ pairs
const (
ASCInvalidOpcode uint8 = 0x20
ASCQLuk uint8 = 0x00
ASCInvalidFieldInCDB uint8 = 0x24
ASCLBAOutOfRange uint8 = 0x21
ASCNotReady uint8 = 0x04
ASCQNotReady uint8 = 0x03 // manual intervention required
)
// BlockDevice is the interface that the SCSI command handler uses to
// interact with the underlying storage. This maps onto BlockVol.
type BlockDevice interface {
ReadAt(lba uint64, length uint32) ([]byte, error)
WriteAt(lba uint64, data []byte) error
Trim(lba uint64, length uint32) error
SyncCache() error
BlockSize() uint32
VolumeSize() uint64 // total size in bytes
IsHealthy() bool
}
// SCSIHandler processes SCSI commands from iSCSI PDUs.
type SCSIHandler struct {
dev BlockDevice
vendorID string // 8 bytes for INQUIRY
prodID string // 16 bytes for INQUIRY
serial string // for VPD page 0x80
}
// NewSCSIHandler creates a SCSI command handler for the given block device.
func NewSCSIHandler(dev BlockDevice) *SCSIHandler {
return &SCSIHandler{
dev: dev,
vendorID: "SeaweedF",
prodID: "BlockVol ",
serial: "SWF00001",
}
}
// SCSIResult holds the result of a SCSI command execution.
type SCSIResult struct {
Status uint8 // SCSI status
Data []byte // Response data (for Data-In)
SenseKey uint8 // Sense key (if CHECK_CONDITION)
SenseASC uint8 // Additional sense code
SenseASCQ uint8 // Additional sense code qualifier
}
// HandleCommand dispatches a SCSI CDB to the appropriate handler.
// dataOut contains any data sent by the initiator (for WRITE commands).
func (h *SCSIHandler) HandleCommand(cdb [16]byte, dataOut []byte) SCSIResult {
opcode := cdb[0]
switch opcode {
case ScsiTestUnitReady:
return h.testUnitReady()
case ScsiRequestSense:
return h.requestSense(cdb)
case ScsiInquiry:
return h.inquiry(cdb)
case ScsiModeSelect6:
return h.modeSelect6(cdb, dataOut)
case ScsiModeSense6:
return h.modeSense6(cdb)
case ScsiStartStopUnit:
return h.startStopUnit(cdb)
case ScsiReadCapacity10:
return h.readCapacity10()
case ScsiRead10:
return h.read10(cdb)
case ScsiWrite10:
if r := h.checkStandbyReject(); r != nil {
return *r
}
return h.write10(cdb, dataOut)
case ScsiSyncCache10:
if r := h.checkStandbyReject(); r != nil {
return *r
}
return h.syncCache()
case ScsiUnmap:
if r := h.checkStandbyReject(); r != nil {
return *r
}
return h.unmap(cdb, dataOut)
case ScsiModeSelect10:
return h.modeSelect10(cdb, dataOut)
case ScsiModeSense10:
return h.modeSense10(cdb)
case ScsiPersistReserveIn:
return h.persistReserveIn(cdb)
case ScsiPersistReserveOut:
return h.persistReserveOut(cdb, dataOut)
case ScsiRead16:
return h.read16(cdb)
case ScsiWrite16:
if r := h.checkStandbyReject(); r != nil {
return *r
}
return h.write16(cdb, dataOut)
case ScsiSyncCache16:
if r := h.checkStandbyReject(); r != nil {
return *r
}
return h.syncCache()
case ScsiWriteSame16:
if r := h.checkStandbyReject(); r != nil {
return *r
}
return h.writeSame16(cdb, dataOut)
case ScsiServiceActionIn16:
sa := cdb[1] & 0x1f
if sa == ScsiSAReadCapacity16 {
return h.readCapacity16(cdb)
}
return illegalRequest(ASCInvalidOpcode, ASCQLuk)
case ScsiReportLuns:
return h.reportLuns(cdb)
case ScsiMaintenanceIn:
return h.maintenanceIn(cdb)
case ScsiMaintenanceOut:
return illegalRequest(ASCInvalidOpcode, ASCQLuk)
default:
return illegalRequest(ASCInvalidOpcode, ASCQLuk)
}
}
// --- Metadata commands ---
func (h *SCSIHandler) testUnitReady() SCSIResult {
if !h.dev.IsHealthy() {
return SCSIResult{
Status: SCSIStatusCheckCond,
SenseKey: SenseNotReady,
SenseASC: ASCNotReady,
SenseASCQ: ASCQNotReady,
}
}
return SCSIResult{Status: SCSIStatusGood}
}
func (h *SCSIHandler) requestSense(cdb [16]byte) SCSIResult {
allocLen := cdb[4]
if allocLen == 0 {
allocLen = 18
}
// Return fixed-format sense data with NO SENSE (no pending error).
data := BuildSenseData(SenseNoSense, 0x00, 0x00)
if int(allocLen) < len(data) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
// startStopUnit handles START STOP UNIT (0x1B).
// Windows sends this during disk init. We accept and ignore.
func (h *SCSIHandler) startStopUnit(cdb [16]byte) SCSIResult {
return SCSIResult{Status: SCSIStatusGood}
}
// modeSelect6 handles MODE SELECT(6) (0x15).
// Windows sends this to set caching mode pages. We accept and ignore the data.
func (h *SCSIHandler) modeSelect6(cdb [16]byte, dataOut []byte) SCSIResult {
return SCSIResult{Status: SCSIStatusGood}
}
// modeSelect10 handles MODE SELECT(10) (0x55).
// Same as modeSelect6 but with 10-byte CDB.
func (h *SCSIHandler) modeSelect10(cdb [16]byte, dataOut []byte) SCSIResult {
return SCSIResult{Status: SCSIStatusGood}
}
// persistReserveIn handles PERSISTENT RESERVE IN (0x5E).
// Windows Cluster and MPIO use this. We don't support reservations,
// so return ILLEGAL_REQUEST with a specific ASC that tells Windows
// "not supported" rather than "broken device".
func (h *SCSIHandler) persistReserveIn(cdb [16]byte) SCSIResult {
return illegalRequest(ASCInvalidOpcode, ASCQLuk)
}
// persistReserveOut handles PERSISTENT RESERVE OUT (0x5F).
func (h *SCSIHandler) persistReserveOut(cdb [16]byte, dataOut []byte) SCSIResult {
return illegalRequest(ASCInvalidOpcode, ASCQLuk)
}
// maintenanceIn handles MAINTENANCE IN (0xA3).
// Service action 0x0A = REPORT TARGET PORT GROUPS (ALUA).
// Service action 0x0C = REPORT SUPPORTED OPERATION CODES.
// Windows sends 0x0C to discover which commands we support.
func (h *SCSIHandler) maintenanceIn(cdb [16]byte) SCSIResult {
sa := cdb[1] & 0x1f
switch sa {
case 0x0a:
return h.reportTargetPortGroups(cdb)
case 0x0c:
// REPORT SUPPORTED OPERATION CODES -- return empty (not supported).
// This tells Windows to probe commands individually.
return illegalRequest(ASCInvalidOpcode, ASCQLuk)
default:
return illegalRequest(ASCInvalidOpcode, ASCQLuk)
}
}
func (h *SCSIHandler) inquiry(cdb [16]byte) SCSIResult {
evpd := cdb[1] & 0x01
pageCode := cdb[2]
allocLen := binary.BigEndian.Uint16(cdb[3:5])
if allocLen == 0 {
allocLen = 36
}
if evpd != 0 {
return h.inquiryVPD(pageCode, allocLen)
}
// Standard INQUIRY response (SPC-5, Section 6.6.1)
data := make([]byte, 96)
data[0] = 0x00 // Peripheral device type: SBC (direct access block device)
data[1] = 0x00 // RMB=0 (not removable)
data[2] = 0x06 // SPC-4 version
data[3] = 0x02 // Response data format = 2 (SPC-2+)
data[4] = 91 // Additional length (96-5)
data[5] = 0x00 // SCCS, ACC, TPGS, 3PC
// If ALUA is supported, set TPGS = implicit (bits 5:4 = 01).
if _, ok := h.dev.(ALUAProvider); ok {
data[5] |= 0x10 // TPGS = 01 (implicit ALUA)
}
data[6] = 0x00 // Obsolete, EncServ, VS, MultiP
data[7] = 0x02 // CmdQue=1 (supports command queuing)
// Vendor ID (bytes 8-15, 8 chars, space padded)
copy(data[8:16], padRight(h.vendorID, 8))
// Product ID (bytes 16-31, 16 chars, space padded)
copy(data[16:32], padRight(h.prodID, 16))
// Product revision (bytes 32-35, 4 chars)
copy(data[32:36], "0001")
if int(allocLen) < len(data) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
func (h *SCSIHandler) inquiryVPD(pageCode uint8, allocLen uint16) SCSIResult {
switch pageCode {
case 0x00: // Supported VPD pages
data := []byte{
0x00, // device type
0x00, // page code
0x00, 0x05, // page length
0x00, // supported pages: 0x00
0x80, // 0x80 (serial)
0x83, // 0x83 (device identification)
0xb0, // 0xB0 (block limits)
0xb2, // 0xB2 (logical block provisioning)
}
if int(allocLen) < len(data) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
case 0x80: // Unit serial number
serial := padRight(h.serial, 8)
data := make([]byte, 4+len(serial))
data[0] = 0x00 // device type
data[1] = 0x80 // page code
binary.BigEndian.PutUint16(data[2:4], uint16(len(serial)))
copy(data[4:], serial)
if int(allocLen) < len(data) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
case 0x83: // Device identification
return h.inquiryVPD83(allocLen)
case 0xb0: // Block Limits (SBC-4, Section 6.6.4)
return h.inquiryVPDB0(allocLen)
case 0xb2: // Logical Block Provisioning (SBC-4, Section 6.6.6)
return h.inquiryVPDB2(allocLen)
default:
return illegalRequest(ASCInvalidFieldInCDB, ASCQLuk)
}
}
// inquiryVPDB0 returns the Block Limits VPD page (0xB0).
// Tells the kernel our maximum WRITE SAME, UNMAP, and transfer lengths.
func (h *SCSIHandler) inquiryVPDB0(allocLen uint16) SCSIResult {
blockSize := h.dev.BlockSize()
totalBlocks := h.dev.VolumeSize() / uint64(blockSize)
// Cap WRITE SAME to the whole volume (no practical limit).
maxWS := totalBlocks
if maxWS > 0xFFFFFFFF {
maxWS = 0xFFFFFFFF
}
data := make([]byte, 64)
data[0] = 0x00 // device type
data[1] = 0xb0 // page code
binary.BigEndian.PutUint16(data[2:4], 0x003c) // page length = 60
// Byte 5: WSNZ (Write Same No Zero) = 0 -- we accept zero-length WRITE SAME
// Bytes 6-7: Maximum compare and write length = 0 (not supported)
// Bytes 8-9: Optimal transfer length granularity = 1 block
binary.BigEndian.PutUint16(data[6:8], 1)
// Bytes 8-11: Maximum transfer length = 0 (no limit from our side)
// Bytes 12-15: Optimal transfer length = 128 blocks (512KB)
binary.BigEndian.PutUint32(data[12:16], 128)
// Bytes 20-23: Maximum prefetch length = 0
// Bytes 24-27: Maximum UNMAP LBA count
binary.BigEndian.PutUint32(data[24:28], uint32(maxWS))
// Bytes 28-31: Maximum UNMAP block descriptor count
binary.BigEndian.PutUint32(data[28:32], 256)
// Bytes 32-35: Optimal UNMAP granularity = 1 block
binary.BigEndian.PutUint32(data[32:36], 1)
// Bytes 36-39: UNMAP granularity alignment (bit 31 = UGAVALID)
// Not set -- no alignment requirement.
// Bytes 40-47: Maximum WRITE SAME length (uint64)
binary.BigEndian.PutUint64(data[40:48], maxWS)
if int(allocLen) < len(data) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
// inquiryVPDB2 returns the Logical Block Provisioning VPD page (0xB2).
// Tells the kernel what thin provisioning features we support.
func (h *SCSIHandler) inquiryVPDB2(allocLen uint16) SCSIResult {
data := make([]byte, 8)
data[0] = 0x00 // device type
data[1] = 0xb2 // page code
binary.BigEndian.PutUint16(data[2:4], 0x0004) // page length = 4
// Byte 5: Provisioning type and flags
// Bits 7: Threshold exponent = 0
data[4] = 0x00
// Byte 5 (offset 5 in page):
// Bit 7: DP (descriptor present) = 0
// Bits 2-0: Provisioning type = 0x02 (thin provisioned)
data[5] = 0x02
// Byte 6: Provisioning group descriptor flags
// Bit 7: LBPU (Logical Block Provisioning UNMAP) = 1 -- we support UNMAP
// Bit 6: LBPWS (LBP Write Same) = 1 -- we support WRITE SAME with UNMAP bit
// Bit 5: LBPWS10 = 0 -- we don't support WRITE SAME(10)
data[6] = 0xC0 // LBPU=1, LBPWS=1
if int(allocLen) < len(data) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
// inquiryVPD83 returns the Device Identification VPD page (0x83).
// When ALUAProvider is available, returns three descriptors needed by dm-multipath:
// 1. NAA-6 identifier (logical unit identity, UUID-derived)
// 2. Target Port Group designator (TPG identity)
// 3. Relative Target Port designator (target port identity)
//
// Without ALUAProvider, returns only the NAA descriptor with a hardcoded value.
func (h *SCSIHandler) inquiryVPD83(allocLen uint16) SCSIResult {
alua, hasALUA := h.dev.(ALUAProvider)
// Descriptor 1: NAA-6 identifier (always present).
var naaBytes [8]byte
if hasALUA {
naaBytes = alua.DeviceNAA()
} else {
naaBytes = [8]byte{0x60, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
}
naaDesc := []byte{
0x01, // code set: binary
0x03, // PIV=0, association=00 (logical unit), type=3 (NAA)
0x00, // reserved
0x08, // identifier length
}
naaDesc = append(naaDesc, naaBytes[:]...)
descs := make([]byte, 0, 32)
descs = append(descs, naaDesc...)
if hasALUA {
tpgID := alua.TPGroupID()
// Descriptor 2: Target Port Group designator (type 0x05).
tpgDesc := []byte{
0x01, // code set: binary
0x15, // PIV=1, association=01 (target port), type=5 (target port group)
0x00, // reserved
0x04, // identifier length
0x00, 0x00, // reserved
byte(tpgID >> 8), byte(tpgID), // TPG ID big-endian
}
descs = append(descs, tpgDesc...)
// Descriptor 3: Relative Target Port designator (type 0x04).
rtpDesc := []byte{
0x01, // code set: binary
0x14, // PIV=1, association=01 (target port), type=4 (relative target port)
0x00, // reserved
0x04, // identifier length
0x00, 0x00, // reserved
0x00, 0x01, // relative target port identifier = 1
}
descs = append(descs, rtpDesc...)
}
data := make([]byte, 4+len(descs))
data[0] = 0x00 // device type
data[1] = 0x83 // page code
binary.BigEndian.PutUint16(data[2:4], uint16(len(descs)))
copy(data[4:], descs)
if int(allocLen) < len(data) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
func (h *SCSIHandler) readCapacity10() SCSIResult {
blockSize := h.dev.BlockSize()
totalBlocks := h.dev.VolumeSize() / uint64(blockSize)
data := make([]byte, 8)
// If >2TB (blocks > 0xFFFFFFFF), return 0xFFFFFFFF to signal use READ_CAPACITY_16
if totalBlocks > 0xFFFFFFFF {
binary.BigEndian.PutUint32(data[0:4], 0xFFFFFFFF)
} else {
binary.BigEndian.PutUint32(data[0:4], uint32(totalBlocks-1)) // last LBA
}
binary.BigEndian.PutUint32(data[4:8], blockSize)
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
func (h *SCSIHandler) readCapacity16(cdb [16]byte) SCSIResult {
allocLen := binary.BigEndian.Uint32(cdb[10:14])
if allocLen < 32 {
allocLen = 32
}
blockSize := h.dev.BlockSize()
totalBlocks := h.dev.VolumeSize() / uint64(blockSize)
data := make([]byte, 32)
binary.BigEndian.PutUint64(data[0:8], totalBlocks-1) // last LBA
binary.BigEndian.PutUint32(data[8:12], blockSize) // block length
// data[12]: LBPME (logical block provisioning management enabled) = 1 for UNMAP support
data[14] = 0x80 // LBPME bit
if allocLen < uint32(len(data)) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
func (h *SCSIHandler) modeSense6(cdb [16]byte) SCSIResult {
allocLen := cdb[4]
if allocLen == 0 {
allocLen = 4
}
pageCode := cdb[2] & 0x3f
// Build mode page data based on requested page.
pages := h.buildModePages(pageCode)
// MODE SENSE(6) header = 4 bytes + pages
data := make([]byte, 4+len(pages))
data[0] = byte(3 + len(pages)) // Mode data length (everything after byte 0)
data[1] = 0x00 // Medium type: default
data[2] = 0x00 // Device-specific parameter (no write protect)
data[3] = 0x00 // Block descriptor length = 0
copy(data[4:], pages)
if int(allocLen) < len(data) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
func (h *SCSIHandler) modeSense10(cdb [16]byte) SCSIResult {
allocLen := binary.BigEndian.Uint16(cdb[7:9])
if allocLen == 0 {
allocLen = 8
}
pageCode := cdb[2] & 0x3f
// Build mode page data based on requested page.
pages := h.buildModePages(pageCode)
// MODE SENSE(10) header = 8 bytes + pages
data := make([]byte, 8+len(pages))
binary.BigEndian.PutUint16(data[0:2], uint16(6+len(pages))) // Mode data length
data[2] = 0x00 // Medium type: default
data[3] = 0x00 // Device-specific parameter (no write protect)
data[4] = 0x00 // Reserved (LONGLBA=0)
data[5] = 0x00 // Reserved
binary.BigEndian.PutUint16(data[6:8], 0) // Block descriptor length = 0
copy(data[8:], pages)
if int(allocLen) < len(data) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
// buildModePages returns the mode page data for the requested page code.
// Page 0x3F = return all pages.
func (h *SCSIHandler) buildModePages(pageCode uint8) []byte {
switch pageCode {
case 0x08: // Caching mode page
return h.modePage08()
case 0x0a: // Control mode page
return h.modePage0A()
case 0x3f: // All pages
var pages []byte
pages = append(pages, h.modePage08()...)
pages = append(pages, h.modePage0A()...)
return pages
default:
return nil
}
}
// modePage08 returns the Caching mode page (SBC-4, Section 7.5.5).
// Tells Windows we support write caching.
func (h *SCSIHandler) modePage08() []byte {
page := make([]byte, 20)
page[0] = 0x08 // Page code
page[1] = 18 // Page length (20 - 2)
page[2] = 0x04 // WCE=1 (Write Cache Enable), RCD=0 (Read Cache Disable)
return page
}
// modePage0A returns the Control mode page (SPC-5, Section 8.4.8).
func (h *SCSIHandler) modePage0A() []byte {
page := make([]byte, 12)
page[0] = 0x0a // Page code
page[1] = 10 // Page length (12 - 2)
return page
}
func (h *SCSIHandler) reportLuns(cdb [16]byte) SCSIResult {
allocLen := binary.BigEndian.Uint32(cdb[6:10])
if allocLen < 16 {
allocLen = 16
}
// Report a single LUN (LUN 0)
data := make([]byte, 16)
binary.BigEndian.PutUint32(data[0:4], 8) // LUN list length (8 bytes, 1 LUN)
// data[4:7] reserved
// data[8:16] = LUN 0 (all zeros)
if allocLen < uint32(len(data)) {
data = data[:allocLen]
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
// --- Data commands (Task 2.6) ---
func (h *SCSIHandler) read10(cdb [16]byte) SCSIResult {
lba := uint64(binary.BigEndian.Uint32(cdb[2:6]))
transferLen := uint32(binary.BigEndian.Uint16(cdb[7:9]))
return h.doRead(lba, transferLen)
}
func (h *SCSIHandler) read16(cdb [16]byte) SCSIResult {
lba := binary.BigEndian.Uint64(cdb[2:10])
transferLen := binary.BigEndian.Uint32(cdb[10:14])
return h.doRead(lba, transferLen)
}
func (h *SCSIHandler) write10(cdb [16]byte, dataOut []byte) SCSIResult {
lba := uint64(binary.BigEndian.Uint32(cdb[2:6]))
transferLen := uint32(binary.BigEndian.Uint16(cdb[7:9]))
return h.doWrite(lba, transferLen, dataOut)
}
func (h *SCSIHandler) write16(cdb [16]byte, dataOut []byte) SCSIResult {
lba := binary.BigEndian.Uint64(cdb[2:10])
transferLen := binary.BigEndian.Uint32(cdb[10:14])
return h.doWrite(lba, transferLen, dataOut)
}
func (h *SCSIHandler) doRead(lba uint64, transferLen uint32) SCSIResult {
if transferLen == 0 {
return SCSIResult{Status: SCSIStatusGood}
}
blockSize := h.dev.BlockSize()
totalBlocks := h.dev.VolumeSize() / uint64(blockSize)
if lba+uint64(transferLen) > totalBlocks {
return illegalRequest(ASCLBAOutOfRange, ASCQLuk)
}
byteLen := transferLen * blockSize
data, err := h.dev.ReadAt(lba, byteLen)
if err != nil {
return SCSIResult{
Status: SCSIStatusCheckCond,
SenseKey: SenseMediumError,
SenseASC: 0x11, // Unrecovered read error
SenseASCQ: 0x00,
}
}
return SCSIResult{Status: SCSIStatusGood, Data: data}
}
func (h *SCSIHandler) doWrite(lba uint64, transferLen uint32, dataOut []byte) SCSIResult {
if transferLen == 0 {
return SCSIResult{Status: SCSIStatusGood}
}
blockSize := h.dev.BlockSize()
totalBlocks := h.dev.VolumeSize() / uint64(blockSize)
if lba+uint64(transferLen) > totalBlocks {
return illegalRequest(ASCLBAOutOfRange, ASCQLuk)
}
expectedBytes := transferLen * blockSize
if uint32(len(dataOut)) < expectedBytes {
return illegalRequest(ASCInvalidFieldInCDB, ASCQLuk)
}
if err := h.dev.WriteAt(lba, dataOut[:expectedBytes]); err != nil {
return mapBlockVolError(err)
}
return SCSIResult{Status: SCSIStatusGood}
}
func (h *SCSIHandler) syncCache() SCSIResult {
if err := h.dev.SyncCache(); err != nil {
return mapBlockVolError(err)
}
return SCSIResult{Status: SCSIStatusGood}
}
func (h *SCSIHandler) unmap(cdb [16]byte, dataOut []byte) SCSIResult {
if len(dataOut) < 8 {
return illegalRequest(ASCInvalidFieldInCDB, ASCQLuk)
}
// UNMAP parameter list header (8 bytes)
// descLen := binary.BigEndian.Uint16(dataOut[0:2]) // data length (unused)
blockDescLen := binary.BigEndian.Uint16(dataOut[2:4])
if int(blockDescLen)+8 > len(dataOut) {
return illegalRequest(ASCInvalidFieldInCDB, ASCQLuk)
}
if blockDescLen%16 != 0 {
return illegalRequest(ASCInvalidFieldInCDB, ASCQLuk)
}
// Each UNMAP block descriptor is 16 bytes
descData := dataOut[8 : 8+blockDescLen]
for len(descData) >= 16 {
lba := binary.BigEndian.Uint64(descData[0:8])
numBlocks := binary.BigEndian.Uint32(descData[8:12])
// descData[12:16] reserved
if numBlocks > 0 {
blockSize := h.dev.BlockSize()
if err := h.dev.Trim(lba, numBlocks*blockSize); err != nil {
return mapBlockVolError(err)
}
}
descData = descData[16:]
}
return SCSIResult{Status: SCSIStatusGood}
}
// writeSame16 handles WRITE SAME(16) -- SBC-4, Section 5.42.
// If UNMAP bit is set, the range is trimmed (zeroed). Otherwise the single
// logical block from dataOut is written repeatedly across the range.
// NDOB (No Data-Out Buffer) means zero the range with no data payload.
func (h *SCSIHandler) writeSame16(cdb [16]byte, dataOut []byte) SCSIResult {
lba := binary.BigEndian.Uint64(cdb[2:10])
numBlocks := binary.BigEndian.Uint32(cdb[10:14])
unmap := cdb[1]&0x08 != 0
ndob := cdb[1]&0x01 != 0
if numBlocks == 0 {
return SCSIResult{Status: SCSIStatusGood}
}
blockSize := h.dev.BlockSize()
totalBlocks := h.dev.VolumeSize() / uint64(blockSize)
if lba+uint64(numBlocks) > totalBlocks {
return illegalRequest(ASCLBAOutOfRange, ASCQLuk)
}
// UNMAP or NDOB: zero/trim the range.
if unmap || ndob {
if err := h.dev.Trim(lba, numBlocks*blockSize); err != nil {
return mapBlockVolError(err)
}
return SCSIResult{Status: SCSIStatusGood}
}
// Normal WRITE SAME: replicate the single block across the range.
if uint32(len(dataOut)) < blockSize {
return illegalRequest(ASCInvalidFieldInCDB, ASCQLuk)
}
pattern := dataOut[:blockSize]
// Check if the pattern is all zeros -- use Trim for efficiency.
allZero := true
for _, b := range pattern {
if b != 0 {
allZero = false
break
}
}
if allZero {
if err := h.dev.Trim(lba, numBlocks*blockSize); err != nil {
return mapBlockVolError(err)
}
return SCSIResult{Status: SCSIStatusGood}
}
// Non-zero pattern: write each block individually.
for i := uint32(0); i < numBlocks; i++ {
if err := h.dev.WriteAt(lba+uint64(i), pattern); err != nil {
return mapBlockVolError(err)
}
}
return SCSIResult{Status: SCSIStatusGood}
}
// isDurabilityError checks if an error originates from the durability layer.
// Uses errors.Is with shared sentinels from blockerr (avoids import cycle
// since blockerr has no dependencies on blockvol or iscsi).
func isDurabilityError(err error) bool {
return errors.Is(err, blockerr.ErrDurabilityBarrierFailed) ||
errors.Is(err, blockerr.ErrDurabilityQuorumLost)
}
// mapBlockVolError maps a blockvol write/sync error to a SCSI sense result.
// Durability errors (barrier failed, quorum lost) are mapped to
// HARDWARE_ERROR + ASC 0x08 (Logical Unit Communication Failure) to
// distinguish them from local I/O errors (MEDIUM_ERROR + ASC 0x0C).
func mapBlockVolError(err error) SCSIResult {
if isDurabilityError(err) {
return SCSIResult{
Status: SCSIStatusCheckCond,
SenseKey: SenseHardwareError,
SenseASC: 0x08, // Logical Unit Communication Failure
SenseASCQ: 0x00,
}
}
return SCSIResult{
Status: SCSIStatusCheckCond,
SenseKey: SenseMediumError,
SenseASC: 0x0C, // Write error
SenseASCQ: 0x00,
}
}
// BuildSenseData constructs a fixed-format sense data buffer (18 bytes).
func BuildSenseData(key, asc, ascq uint8) []byte {
data := make([]byte, 18)
data[0] = 0x70 // Response code: current errors, fixed format
data[2] = key & 0x0f // Sense key
data[7] = 10 // Additional sense length
data[12] = asc // ASC
data[13] = ascq // ASCQ
return data
}
func illegalRequest(asc, ascq uint8) SCSIResult {
return SCSIResult{
Status: SCSIStatusCheckCond,
SenseKey: SenseIllegalRequest,
SenseASC: asc,
SenseASCQ: ascq,
}
}
func padRight(s string, n int) string {
if len(s) >= n {
return s[:n]
}
b := make([]byte, n)
copy(b, s)
for i := len(s); i < n; i++ {
b[i] = ' '
}
return string(b)
}