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/dev_block.go b/metropolis/pkg/scsi/dev_block.go
new file mode 100644
index 0000000..1941d83
--- /dev/null
+++ b/metropolis/pkg/scsi/dev_block.go
@@ -0,0 +1,155 @@
+package scsi
+
+// Written against SBC-4
+// Contains SCSI block device specific commands.
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"math"
+)
+
+// ReadDefectDataLBA reads the primary (manufacturer) and/or grown defect list
+// in LBA format. This is commonly used on SSDs and generally returns an error
+// on spinning drives.
+func (d *Device) ReadDefectDataLBA(plist, glist bool) ([]uint64, error) {
+	data := make([]byte, 4096)
+	var req [8]byte
+	if plist {
+		req[1] |= 1 << 4
+	}
+	if glist {
+		req[1] |= 1 << 3
+	}
+	defectListFormat := 0b011
+	req[1] |= byte(defectListFormat)
+	binary.BigEndian.PutUint16(req[6:8], uint16(len(data)))
+	if err := d.RawCommand(&CommandDataBuffer{
+		OperationCode:         ReadDefectDataOp,
+		Request:               req[:],
+		Data:                  data,
+		DataTransferDirection: DataTransferFromDevice,
+	}); err != nil {
+		var fixedErr *FixedError
+		if errors.As(err, &fixedErr) && fixedErr.SenseKey == RecoveredError && fixedErr.AdditionalSenseCode == DefectListNotFound {
+			return nil, fmt.Errorf("error during LOG SENSE: unsupported defect list format, device returned %03bb", data[1]&0b111)
+		}
+		return nil, fmt.Errorf("error during LOG SENSE: %w", err)
+	}
+	if data[1]&0b111 != byte(defectListFormat) {
+		return nil, fmt.Errorf("device returned wrong defect list format, requested %03bb, got %03bb", defectListFormat, data[1]&0b111)
+	}
+	defectListLength := binary.BigEndian.Uint16(data[2:4])
+	if defectListLength%8 != 0 {
+		return nil, errors.New("returned defect list not divisible by array item size")
+	}
+	res := make([]uint64, defectListLength/8)
+	if err := binary.Read(bytes.NewReader(data[4:]), binary.BigEndian, &res); err != nil {
+		panic(err)
+	}
+	return res, nil
+}
+
+const (
+	// AllSectors is a magic sector number indicating that it applies to all
+	// sectors on the track.
+	AllSectors = math.MaxUint16
+)
+
+// PhysicalSectorFormatAddress represents a physical sector (or the the whole
+// track if SectorNumber == AllSectors) on a spinning hard drive.
+type PhysicalSectorFormatAddress struct {
+	CylinderNumber              uint32
+	HeadNumber                  uint8
+	SectorNumber                uint32
+	MultiAddressDescriptorStart bool
+}
+
+func parseExtendedPhysicalSectorFormatAddress(buf []byte) (p PhysicalSectorFormatAddress) {
+	p.CylinderNumber = uint32(buf[0])<<16 | uint32(buf[1])<<8 | uint32(buf[2])
+	p.HeadNumber = buf[3]
+	p.MultiAddressDescriptorStart = buf[4]&(1<<7) != 0
+	p.SectorNumber = uint32(buf[4]&0b1111)<<24 | uint32(buf[5])<<16 | uint32(buf[6])<<8 | uint32(buf[7])
+	return
+}
+
+func parsePhysicalSectorFormatAddress(buf []byte) (p PhysicalSectorFormatAddress) {
+	p.CylinderNumber = uint32(buf[0])<<16 | uint32(buf[1])<<8 | uint32(buf[2])
+	p.HeadNumber = buf[3]
+	p.SectorNumber = binary.BigEndian.Uint32(buf[4:8])
+	return
+}
+
+// ReadDefectDataPhysical reads the primary (manufacturer) and/or grown defect
+// list in physical format.
+// This is only defined for spinning drives, returning an error on SSDs.
+func (d *Device) ReadDefectDataPhysical(plist, glist bool) ([]PhysicalSectorFormatAddress, error) {
+	data := make([]byte, 4096)
+	var req [8]byte
+	if plist {
+		req[1] |= 1 << 4
+	}
+	if glist {
+		req[1] |= 1 << 3
+	}
+	defectListFormat := 0b101
+	req[1] |= byte(defectListFormat)
+	binary.BigEndian.PutUint16(req[6:8], uint16(len(data)))
+	if err := d.RawCommand(&CommandDataBuffer{
+		OperationCode:         ReadDefectDataOp,
+		Request:               req[:],
+		Data:                  data,
+		DataTransferDirection: DataTransferFromDevice,
+	}); err != nil {
+		var fixedErr *FixedError
+		if errors.As(err, &fixedErr) && fixedErr.SenseKey == RecoveredError && fixedErr.AdditionalSenseCode == DefectListNotFound {
+			return nil, fmt.Errorf("error during LOG SENSE: unsupported defect list format, device returned %03bb", data[1]&0b111)
+		}
+		return nil, fmt.Errorf("error during LOG SENSE: %w", err)
+	}
+	if data[1]&0b111 != byte(defectListFormat) {
+		return nil, fmt.Errorf("device returned wrong defect list format, requested %03bb, got %03bb", defectListFormat, data[1]&0b111)
+	}
+	defectListLength := binary.BigEndian.Uint16(data[2:4])
+	if defectListLength%8 != 0 {
+		return nil, errors.New("returned defect list not divisible by array item size")
+	}
+	if len(data) < int(defectListLength)+4 {
+		return nil, errors.New("returned defect list longer than buffer")
+	}
+	res := make([]PhysicalSectorFormatAddress, defectListLength/8)
+	data = data[4:]
+	for i := 0; i < int(defectListLength)/8; i++ {
+		res[i] = parsePhysicalSectorFormatAddress(data[i*8 : (i+1)*8])
+	}
+	return res, nil
+}
+
+type SolidStateMediaHealth struct {
+	// PercentageUsedEnduranceIndicator is a value which represents a
+	// vendor-specific wear estimate of the solid state medium.
+	// A new device starts at 0, at 100 the device is considered end-of-life.
+	// Values up to 255 are possible.
+	PercentageUsedEnduranceIndicator uint8
+}
+
+// SolidStateMediaHealth reports parameters about the health of the solid-state
+// media of a SCSI block device.
+func (d *Device) SolidStateMediaHealth() (*SolidStateMediaHealth, error) {
+	raw, err := d.LogSenseParameters(LogSenseRequest{PageCode: 0x11})
+	if err != nil {
+		return nil, err
+	}
+	if len(raw[0x1]) == 0 {
+		return nil, errors.New("mandatory parameter 0001h missing")
+	}
+	param1 := raw[0x01][0]
+	if len(param1.Data) < 4 {
+		return nil, errors.New("parameter 0001h too short")
+	}
+	return &SolidStateMediaHealth{
+		PercentageUsedEnduranceIndicator: param1.Data[3],
+	}, nil
+}