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/inquiry.go b/metropolis/pkg/scsi/inquiry.go
new file mode 100644
index 0000000..45c1603
--- /dev/null
+++ b/metropolis/pkg/scsi/inquiry.go
@@ -0,0 +1,299 @@
+package scsi
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"io"
+	"math"
+)
+
+// Inquiry queries the device for various metadata about its identity and
+// supported features.
+func (d Device) Inquiry() (*InquiryData, error) {
+	data := make([]byte, 96)
+	var req [4]byte
+	binary.BigEndian.PutUint16(req[2:4], uint16(len(data)))
+	if err := d.RawCommand(&CommandDataBuffer{
+		OperationCode:         InquiryOp,
+		Request:               req[:],
+		Data:                  data,
+		DataTransferDirection: DataTransferFromDevice,
+	}); err != nil {
+		return nil, fmt.Errorf("error during INQUIRY: %w", err)
+	}
+	resLen := int64(data[4]) + 5
+	// Use LimitReader to not have to deal with out-of-bounds slices
+	rawReader := io.LimitReader(bytes.NewReader(data), resLen)
+	var raw inquiryDataRaw
+	if err := binary.Read(rawReader, binary.BigEndian, &raw); err != nil {
+		if err == io.ErrUnexpectedEOF {
+			return nil, fmt.Errorf("response to INQUIRY is smaller than %d bytes, very old or broken device", binary.Size(raw))
+		}
+		panic(err) // Read from memory, shouldn't be possible to hit
+	}
+
+	var res InquiryData
+	res.PeriperalQualifier = (raw.PeripheralData >> 5) & 0b111
+	res.PeripheralDeviceType = DeviceType(raw.PeripheralData & 0b11111)
+	res.RemovableMedium = (raw.Flags1 & 1 << 0) != 0
+	res.LogicalUnitConglomerate = (raw.Flags1 & 1 << 1) != 0
+	res.CommandSetVersion = Version(raw.Version)
+	res.NormalACASupported = (raw.Flags2 & 1 << 5) != 0
+	res.HistoricalSupport = (raw.Flags2 & 1 << 4) != 0
+	res.ResponseDataFormat = raw.Flags2 & 0b1111
+	res.SCCSupported = (raw.Flags3 & 1 << 7) != 0
+	res.TargetPortGroupSupport = (raw.Flags3 >> 4) & 0b11
+	res.ThirdPartyCopySupport = (raw.Flags3 & 1 << 3) != 0
+	res.HasProtectionInfo = (raw.Flags3 & 1 << 0) != 0
+	res.HasEnclosureServices = (raw.Flags4 & 1 << 6) != 0
+	res.VendorFeature1 = (raw.Flags4 & 1 << 5) != 0
+	res.HasMultipleSCSIPorts = (raw.Flags4 & 1 << 4) != 0
+	res.CmdQueue = (raw.Flags5 & 1 << 1) != 0
+	res.VendorFeature2 = (raw.Flags5 & 1 << 0) != 0
+	res.Vendor = string(bytes.TrimRight(raw.Vendor[:], " "))
+	res.Product = string(bytes.TrimRight(raw.Product[:], " "))
+	res.ProductRevisionLevel = string(bytes.TrimRight(raw.ProductRevisionLevel[:], " "))
+
+	// Read rest conditionally, as it might not be present on every device
+	var vendorSpecific bytes.Buffer
+	_, err := io.CopyN(&vendorSpecific, rawReader, 20)
+	res.VendorSpecific = vendorSpecific.Bytes()
+	if err == io.EOF {
+		return &res, nil
+	}
+	if err != nil {
+		panic(err) // Mem2Mem copy, can't really happen
+	}
+	var padding [2]byte
+	if _, err := io.ReadFull(rawReader, padding[:]); err != nil {
+		if err == io.ErrUnexpectedEOF {
+			return &res, nil
+		}
+	}
+	for i := 0; i < 8; i++ {
+		var versionDesc uint16
+		if err := binary.Read(rawReader, binary.BigEndian, &versionDesc); err != nil {
+			if err == io.EOF || err == io.ErrUnexpectedEOF {
+				return &res, nil
+			}
+		}
+		res.VersionDescriptors = append(res.VersionDescriptors, versionDesc)
+	}
+
+	return &res, nil
+}
+
+// Table 148, only first 36 mandatory bytes
+type inquiryDataRaw struct {
+	PeripheralData       uint8
+	Flags1               uint8
+	Version              uint8
+	Flags2               uint8
+	AdditionalLength     uint8 // n-4
+	Flags3               uint8
+	Flags4               uint8
+	Flags5               uint8
+	Vendor               [8]byte
+	Product              [16]byte
+	ProductRevisionLevel [4]byte
+}
+
+// DeviceType represents a SCSI peripheral device type, which
+// can be used to determine the command set to use to control
+// the device. See Table 150 in the standard.
+type DeviceType uint8
+
+const (
+	TypeBlockDevice              DeviceType = 0x00
+	TypeSequentialAccessDevice   DeviceType = 0x01
+	TypeProcessor                DeviceType = 0x03
+	TypeOpticalDrive             DeviceType = 0x05
+	TypeOpticalMemory            DeviceType = 0x07
+	TypeMediaChanger             DeviceType = 0x08
+	TypeArrayController          DeviceType = 0x0c
+	TypeEncloseServices          DeviceType = 0x0d
+	TypeOpticalCardRWDevice      DeviceType = 0x0f
+	TypeObjectStorageDevice      DeviceType = 0x11
+	TypeAutomationDriveInterface DeviceType = 0x12
+	TypeZonedBlockDevice         DeviceType = 0x14
+	TypeUnknownDevice            DeviceType = 0x1f
+)
+
+var deviceTypeDesc = map[DeviceType]string{
+	TypeBlockDevice:              "Block Device",
+	TypeSequentialAccessDevice:   "Sequential Access Device",
+	TypeProcessor:                "Processor",
+	TypeOpticalDrive:             "Optical Drive",
+	TypeOpticalMemory:            "Optical Memory",
+	TypeMediaChanger:             "Media Changer",
+	TypeArrayController:          "Array Controller",
+	TypeEncloseServices:          "Enclosure Services",
+	TypeOpticalCardRWDevice:      "Optical Card reader/writer device",
+	TypeObjectStorageDevice:      "Object-based Storage Device",
+	TypeAutomationDriveInterface: "Automation/Drive Interface",
+	TypeZonedBlockDevice:         "Zoned Block Device",
+	TypeUnknownDevice:            "Unknown or no device",
+}
+
+func (d DeviceType) String() string {
+	if str, ok := deviceTypeDesc[d]; ok {
+		return str
+	}
+	return fmt.Sprintf("unknown device type %xh", uint8(d))
+}
+
+// Version represents a specific standardized version of the SCSI
+// primary command set (SPC). The enum values are sorted, so
+// for example version >= SPC3 is true for SPC-3 and all later
+// standards. See table 151 in the standard.
+type Version uint8
+
+const (
+	SPC1 = 0x03
+	SPC2 = 0x04
+	SPC3 = 0x05
+	SPC4 = 0x06
+	SPC5 = 0x07
+)
+
+var versionDesc = map[Version]string{
+	SPC1: "SPC-1 (INCITS 301-1997)",
+	SPC2: "SPC-2 (INCITS 351-2001)",
+	SPC3: "SPC-3 (INCITS 408-2005)",
+	SPC4: "SPC-4 (INCITS 513-2015)",
+	SPC5: "SPC-5 (INCITS 502-2019)",
+}
+
+func (v Version) String() string {
+	if str, ok := versionDesc[v]; ok {
+		return str
+	}
+	return fmt.Sprintf("unknown version %xh", uint8(v))
+}
+
+// InquiryData contains data returned by the INQUIRY command.
+type InquiryData struct {
+	PeriperalQualifier      uint8
+	PeripheralDeviceType    DeviceType
+	RemovableMedium         bool
+	LogicalUnitConglomerate bool
+	CommandSetVersion       Version
+	NormalACASupported      bool
+	HistoricalSupport       bool
+	ResponseDataFormat      uint8
+	SCCSupported            bool
+	TargetPortGroupSupport  uint8
+	ThirdPartyCopySupport   bool
+	HasProtectionInfo       bool
+	HasEnclosureServices    bool
+	VendorFeature1          bool
+	HasMultipleSCSIPorts    bool
+	CmdQueue                bool
+	VendorFeature2          bool
+	Vendor                  string
+	Product                 string
+	ProductRevisionLevel    string
+	VendorSpecific          []byte
+	VersionDescriptors      []uint16
+}
+
+// Table 498
+type VPDPageCode uint8
+
+const (
+	SupportedVPDs                      VPDPageCode = 0x00
+	UnitSerialNumberVPD                VPDPageCode = 0x80
+	DeviceIdentificationVPD            VPDPageCode = 0x83
+	SoftwareInterfaceIdentificationVPD VPDPageCode = 0x84
+	ManagementNetworkAddressesVPD      VPDPageCode = 0x85
+	ExtendedINQUIRYDataVPD             VPDPageCode = 0x86
+	ModePagePolicyVPD                  VPDPageCode = 0x87
+	SCSIPortsVPD                       VPDPageCode = 0x88
+	ATAInformationVPD                  VPDPageCode = 0x89
+	PowerConditionVPD                  VPDPageCode = 0x8a
+	DeviceConstituentsVPD              VPDPageCode = 0x8b
+)
+
+var vpdPageCodeDesc = map[VPDPageCode]string{
+	SupportedVPDs:                      "Supported VPD Pages",
+	UnitSerialNumberVPD:                "Unit Serial Number",
+	DeviceIdentificationVPD:            "Device Identification",
+	SoftwareInterfaceIdentificationVPD: "Software Interface Identification",
+	ManagementNetworkAddressesVPD:      "Management Network Addresses",
+	ExtendedINQUIRYDataVPD:             "Extended INQUIRY Data",
+	ModePagePolicyVPD:                  "Mode Page Policy",
+	SCSIPortsVPD:                       "SCSI Ports",
+	ATAInformationVPD:                  "ATA Information",
+	PowerConditionVPD:                  "Power Condition",
+	DeviceConstituentsVPD:              "Device Constituents",
+}
+
+func (v VPDPageCode) String() string {
+	if str, ok := vpdPageCodeDesc[v]; ok {
+		return str
+	}
+	return fmt.Sprintf("Page %xh", uint8(v))
+}
+
+// InquiryVPD requests a specified Vital Product Description Page from the
+// device. If the size of the page is known in advance, initialSize should be
+// set to a non-zero value to make the query more efficient.
+func (d *Device) InquiryVPD(pageCode VPDPageCode, initialSize uint16) ([]byte, error) {
+	var bufferSize uint16 = 254
+	if initialSize > 0 {
+		bufferSize = initialSize
+	}
+	for {
+		data := make([]byte, bufferSize)
+		var req [4]byte
+		req[0] = 0b1 // Enable Vital Product Data
+		req[1] = uint8(pageCode)
+		binary.BigEndian.PutUint16(req[2:4], uint16(len(data)))
+		if err := d.RawCommand(&CommandDataBuffer{
+			OperationCode:         InquiryOp,
+			Request:               req[:],
+			Data:                  data,
+			DataTransferDirection: DataTransferFromDevice,
+		}); err != nil {
+			return nil, fmt.Errorf("error during INQUIRY VPD: %w", err)
+		}
+		if data[1] != uint8(pageCode) {
+			return nil, fmt.Errorf("requested VPD page %x, got %x", pageCode, data[1])
+		}
+		pageLength := binary.BigEndian.Uint16(data[2:4])
+		if pageLength > math.MaxUint16-4 {
+			// Guard against uint16 overflows, this cannot be requested anyway
+			return nil, fmt.Errorf("device VPD page is too long (%d bytes)", pageLength)
+		}
+		if pageLength > uint16(len(data)-4) {
+			bufferSize = pageLength + 4
+			continue
+		}
+		return data[4 : pageLength+4], nil
+	}
+}
+
+// SupportedVPDPages returns the list of supported vital product data pages
+// supported by the device.
+func (d *Device) SupportedVPDPages() (map[VPDPageCode]bool, error) {
+	res, err := d.InquiryVPD(SupportedVPDs, 0)
+	if err != nil {
+		return nil, err
+	}
+	supportedPages := make(map[VPDPageCode]bool)
+	for _, p := range res {
+		supportedPages[VPDPageCode(p)] = true
+	}
+	return supportedPages, nil
+}
+
+// UnitSerialNumber returns the serial number of the device. Only available if
+// UnitSerialNumberVPD is a supported VPD page.
+func (d *Device) UnitSerialNumber() (string, error) {
+	serial, err := d.InquiryVPD(UnitSerialNumberVPD, 0)
+	if err != nil {
+		return "", err
+	}
+	return string(bytes.Trim(serial, " \x00")), nil
+}