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_linux.go b/metropolis/pkg/scsi/scsi_linux.go
new file mode 100644
index 0000000..be937d0
--- /dev/null
+++ b/metropolis/pkg/scsi/scsi_linux.go
@@ -0,0 +1,96 @@
+//go:build linux
+
+package scsi
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"math"
+	"runtime"
+	"unsafe"
+
+	"golang.org/x/sys/unix"
+)
+
+// RawCommand issues a raw command against the device.
+func (d *Device) RawCommand(c *CommandDataBuffer) error {
+	cdb, err := c.Bytes()
+	if err != nil {
+		return fmt.Errorf("error encoding CDB: %w", err)
+	}
+	conn, err := d.fd.SyscallConn()
+	if err != nil {
+		return fmt.Errorf("unable to get RawConn: %w", err)
+	}
+	var dxferDir int32
+	switch c.DataTransferDirection {
+	case DataTransferNone:
+		dxferDir = SG_DXFER_NONE
+	case DataTransferFromDevice:
+		dxferDir = SG_DXFER_FROM_DEV
+	case DataTransferToDevice:
+		dxferDir = SG_DXFER_TO_DEV
+	case DataTransferBidirectional:
+		dxferDir = SG_DXFER_TO_FROM_DEV
+	default:
+		return errors.New("invalid DataTransferDirection")
+	}
+	var timeout uint32
+	if c.Timeout.Milliseconds() > math.MaxUint32 {
+		timeout = math.MaxUint32
+	}
+	if len(c.Data) > math.MaxUint32 {
+		return errors.New("payload larger than 2^32 bytes, unable to issue")
+	}
+	if len(cdb) > math.MaxUint8 {
+		return errors.New("CDB larger than 2^8 bytes, unable to issue")
+	}
+	var senseBuf [32]byte
+	cmdRaw := sgIOHdr{
+		Interface_id:    'S',
+		Dxfer_direction: dxferDir,
+		Dxfer_len:       uint32(len(c.Data)),
+		Dxferp:          uintptr(unsafe.Pointer(&c.Data[0])),
+		Cmd_len:         uint8(len(cdb)),
+		Cmdp:            uintptr(unsafe.Pointer(&cdb[0])),
+		Mx_sb_len:       uint8(len(senseBuf)),
+		Sbp:             uintptr(unsafe.Pointer(&senseBuf[0])),
+		Timeout:         timeout,
+	}
+	var errno unix.Errno
+	err = conn.Control(func(fd uintptr) {
+		_, _, errno = unix.Syscall(unix.SYS_IOCTL, fd, SG_IO, uintptr(unsafe.Pointer(&cmdRaw)))
+	})
+	runtime.KeepAlive(cmdRaw)
+	runtime.KeepAlive(c.Data)
+	runtime.KeepAlive(senseBuf)
+	runtime.KeepAlive(cdb)
+	if err != nil {
+		return fmt.Errorf("unable to get fd: %w", err)
+	}
+	if errno != 0 {
+		return errno
+	}
+	if cmdRaw.Masked_status != 0 {
+		if senseBuf[0] == 0x70 || senseBuf[0] == 0x71 {
+			err := &FixedError{
+				Deferred:    senseBuf[0] == 0x71,
+				SenseKey:    SenseKey(senseBuf[2] & 0b1111),
+				Information: binary.BigEndian.Uint32(senseBuf[3:7]),
+			}
+			length := int(senseBuf[7])
+			if length >= 4 {
+				err.CommandSpecificInformation = binary.BigEndian.Uint32(senseBuf[8:12])
+				if length >= 6 {
+					err.AdditionalSenseCode = AdditionalSenseCode(uint16(senseBuf[12])<<8 | uint16(senseBuf[13]))
+				}
+			}
+			return err
+		}
+		return &UnknownError{
+			RawSenseData: senseBuf[:],
+		}
+	}
+	return nil
+}