m/p/scsi: add SCSI package
This adds a SCSI package to interact with SCSI devices.
It implements a subset of commands from the SPC-5 and SBC-4 standard
useful for discovery and health assessment.
A follow-up will add SAT (SCSI-to-ATA translation) support.
Change-Id: I7f084d26f11d9c951f51051040160e351cf5594c
Reviewed-on: https://review.monogon.dev/c/monogon/+/1066
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/pkg/scsi/scsi.go b/metropolis/pkg/scsi/scsi.go
new file mode 100644
index 0000000..e2d236e
--- /dev/null
+++ b/metropolis/pkg/scsi/scsi.go
@@ -0,0 +1,256 @@
+// INCITS 502 Revision 19 / SPC-5 R19
+package scsi
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "syscall"
+ "time"
+)
+
+// Device is a handle for a SCSI device
+type Device struct {
+ fd syscall.Conn
+}
+
+// NewFromFd creates a new SCSI device handle from a system handle.
+func NewFromFd(fd syscall.Conn) (*Device, error) {
+ d := &Device{fd: fd}
+ // There is no good way to validate that a file descriptor indeed points to
+ // a SCSI device. For future compatibility let this return an error so that
+ // code is already prepared to handle it.
+ return d, nil
+}
+
+// Open creates a new SCSI device handle from a device path (like /dev/sda).
+func Open(path string) (*Device, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("unable to open path: %w", err)
+ }
+ return NewFromFd(f)
+}
+
+// Close closes the SCSI device handle if opened by Open()
+func (d *Device) Close() error {
+ if f, ok := d.fd.(*os.File); ok {
+ return f.Close()
+ } else {
+ return errors.New("unable to close device not opened via Open, please close it yourself")
+ }
+}
+
+type DataTransferDirection uint8
+
+const (
+ DataTransferNone DataTransferDirection = iota
+ DataTransferToDevice
+ DataTransferFromDevice
+ DataTransferBidirectional
+)
+
+type OperationCode uint8
+
+const (
+ InquiryOp OperationCode = 0x12
+ ReadDefectDataOp OperationCode = 0x37
+ LogSenseOp OperationCode = 0x4d
+)
+
+// CommandDataBuffer represents a command
+type CommandDataBuffer struct {
+ // OperationCode contains the code of the command to be called
+ OperationCode OperationCode
+ // Request contains the OperationCode-specific request parameters
+ Request []byte
+ // ServiceAction can (for certain CDB encodings) contain an additional
+ // qualification for the OperationCode.
+ ServiceAction *uint8
+ // Control contains common CDB metadata
+ Control uint8
+ // DataTransferDirection contains the direction(s) of the data transfer(s)
+ // to be made.
+ DataTransferDirection DataTransferDirection
+ // Data contains the data to be transferred. If data needs to be received
+ // from the device, a buffer needs to be provided here.
+ Data []byte
+ // Timeout can contain an optional timeout (0 = no timeout) for the command
+ Timeout time.Duration
+}
+
+// Bytes returns the raw CDB to be sent to the device
+func (c *CommandDataBuffer) Bytes() ([]byte, error) {
+ // Table 24
+ switch {
+ case c.OperationCode < 0x20:
+ // Use CDB6 as defined in Table 3
+ if c.ServiceAction != nil {
+ return nil, errors.New("ServiceAction field not available in CDB6")
+ }
+ if len(c.Request) != 4 {
+ return nil, fmt.Errorf("CDB6 request size is %d bytes, needs to be 4 bytes without LengthField", len(c.Request))
+ }
+
+ outBuf := make([]byte, 6)
+ outBuf[0] = uint8(c.OperationCode)
+
+ copy(outBuf[1:5], c.Request)
+ outBuf[5] = c.Control
+ return outBuf, nil
+ case c.OperationCode < 0x60:
+ // Use CDB10 as defined in Table 5
+ if len(c.Request) != 8 {
+ return nil, fmt.Errorf("CDB10 request size is %d bytes, needs to be 4 bytes", len(c.Request))
+ }
+
+ outBuf := make([]byte, 10)
+ outBuf[0] = uint8(c.OperationCode)
+ copy(outBuf[1:9], c.Request)
+ if c.ServiceAction != nil {
+ outBuf[1] |= *c.ServiceAction & 0b11111
+ }
+ outBuf[9] = c.Control
+ return outBuf, nil
+ case c.OperationCode < 0x7e:
+ return nil, errors.New("OperationCode is reserved")
+ case c.OperationCode == 0x7e:
+ // Use variable extended
+ return nil, errors.New("variable extended CDBs are unimplemented")
+ case c.OperationCode == 0x7f:
+ // Use variable
+ return nil, errors.New("variable CDBs are unimplemented")
+ case c.OperationCode < 0xa0:
+ // Use CDB16 as defined in Table 13
+ if len(c.Request) != 14 {
+ return nil, fmt.Errorf("CDB16 request size is %d bytes, needs to be 14 bytes", len(c.Request))
+ }
+
+ outBuf := make([]byte, 16)
+ outBuf[0] = uint8(c.OperationCode)
+ copy(outBuf[1:15], c.Request)
+ if c.ServiceAction != nil {
+ outBuf[1] |= *c.ServiceAction & 0b11111
+ }
+ outBuf[15] = c.Control
+ return outBuf, nil
+ case c.OperationCode < 0xc0:
+ // Use CDB12 as defined in Table 7
+ if len(c.Request) != 10 {
+ return nil, fmt.Errorf("CDB12 request size is %d bytes, needs to be 10 bytes", len(c.Request))
+ }
+
+ outBuf := make([]byte, 12)
+ outBuf[0] = uint8(c.OperationCode)
+ copy(outBuf[1:11], c.Request)
+ if c.ServiceAction != nil {
+ outBuf[1] |= *c.ServiceAction & 0b11111
+ }
+ outBuf[11] = c.Control
+ return outBuf, nil
+ default:
+ return nil, errors.New("unable to encode CDB for given OperationCode")
+ }
+}
+
+// SenseKey represents the top-level status code of a SCSI sense response.
+type SenseKey uint8
+
+const (
+ NoSense SenseKey = 0x0
+ RecoveredError SenseKey = 0x1
+ NotReady SenseKey = 0x2
+ MediumError SenseKey = 0x3
+ HardwareError SenseKey = 0x4
+ IllegalRequest SenseKey = 0x5
+ UnitAttention SenseKey = 0x6
+ DataProtect SenseKey = 0x7
+ BlankCheck SenseKey = 0x8
+ VendorSpecific SenseKey = 0x9
+ CopyAborted SenseKey = 0xa
+ AbortedCommand SenseKey = 0xb
+ VolumeOverflow SenseKey = 0xd
+ Miscompare SenseKey = 0xe
+ Completed SenseKey = 0xf
+)
+
+var senseKeyDesc = map[SenseKey]string{
+ NoSense: "no sense information",
+ RecoveredError: "recovered error",
+ NotReady: "not ready",
+ MediumError: "medium error",
+ HardwareError: "hardware error",
+ IllegalRequest: "illegal request",
+ UnitAttention: "unit attention",
+ DataProtect: "data protected",
+ BlankCheck: "blank check failed",
+ VendorSpecific: "vendor-specific error",
+ CopyAborted: "third-party copy aborted",
+ AbortedCommand: "command aborted",
+ VolumeOverflow: "volume overflow",
+ Miscompare: "miscompare",
+ Completed: "completed",
+}
+
+func (s SenseKey) String() string {
+ if str, ok := senseKeyDesc[s]; ok {
+ return str
+ }
+ return fmt.Sprintf("sense key %xh", uint8(s))
+}
+
+// AdditionalSenseCode contains the additional sense key and qualifier in one
+// 16-bit value. The high 8 bits are the sense key, the bottom 8 bits the
+// qualifier.
+type AdditionalSenseCode uint16
+
+// ASK returns the raw Additional Sense Key
+func (a AdditionalSenseCode) ASK() uint8 {
+ return uint8(a >> 8)
+}
+
+// ASKQ returns the raw Additional Sense Key Qualifier
+func (a AdditionalSenseCode) ASKQ() uint8 {
+ return uint8(a & 0xFF)
+}
+
+// IsKey checks if the ASK portion of a is the same as the ASK portion of b.
+func (a AdditionalSenseCode) IsKey(b AdditionalSenseCode) bool {
+ return a.ASK() == b.ASK()
+}
+
+// String returns the textual representation of this ASK
+func (s AdditionalSenseCode) String() string {
+ if str, ok := additionalSenseCodeDesc[s]; ok {
+ return str
+ }
+ return fmt.Sprintf("unknown additional sense code %xh %xh", s.ASK(), s.ASKQ())
+}
+
+// FixedError is one type of error returned by a SCSI CHECK_CONDITION.
+// See also Table 48 in the standard.
+type FixedError struct {
+ Deferred bool
+ SenseKey SenseKey
+ Information uint32
+ CommandSpecificInformation uint32
+ AdditionalSenseCode AdditionalSenseCode
+}
+
+func (e FixedError) Error() string {
+ if e.AdditionalSenseCode == 0 {
+ return fmt.Sprintf("%v", e.SenseKey)
+ }
+ return fmt.Sprintf("%v: %v", e.SenseKey, e.AdditionalSenseCode)
+
+}
+
+// UnknownError is a type of error returned by SCSI which is not understood by this
+// library. This can be a vendor-specific or future error.
+type UnknownError struct {
+ RawSenseData []byte
+}
+
+func (e *UnknownError) Error() string {
+ return fmt.Sprintf("unknown SCSI error, raw sense data follows: %x", e.RawSenseData)
+}