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/log.go b/metropolis/pkg/scsi/log.go
new file mode 100644
index 0000000..3aecd5e
--- /dev/null
+++ b/metropolis/pkg/scsi/log.go
@@ -0,0 +1,174 @@
+package scsi
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"math"
+)
+
+type LogSenseRequest struct {
+	// PageCode contains the identifier of the requested page
+	PageCode uint8
+	// SubpageCode contains the identifier of the requested subpage
+	// or the zero value if no subpage is requested.
+	SubpageCode uint8
+	// PageControl specifies what type of values should be returned for bounded
+	// and unbounded log parameters. See also Table 156 in the standard.
+	PageControl uint8
+	// ParameterPointer allows requesting parameter data beginning from a
+	// specific parameter code. The zero value starts from the beginning.
+	ParameterPointer uint16
+	// SaveParameters requests the device to save all parameters without
+	// DisableUpdate set to non-volatile storage.
+	SaveParameters bool
+	// InitialSize is an optional hint how big the buffer for the log page
+	// should be for the initial request. The zero value sets this to 4096.
+	InitialSize uint16
+}
+
+// LogSenseRaw requests a raw log page. For log pages with parameters
+// LogSenseParameters is better-suited.
+func (d *Device) LogSenseRaw(r LogSenseRequest) ([]byte, error) {
+	var bufferSize uint16 = 4096
+	for {
+		data := make([]byte, bufferSize)
+		var req [8]byte
+		if r.SaveParameters {
+			req[0] = 0b1
+		}
+		req[1] = r.PageControl<<6 | r.PageCode
+		req[2] = r.SubpageCode
+		binary.BigEndian.PutUint16(req[4:6], r.ParameterPointer)
+		binary.BigEndian.PutUint16(req[6:8], uint16(len(data)))
+		if err := d.RawCommand(&CommandDataBuffer{
+			OperationCode:         LogSenseOp,
+			Request:               req[:],
+			Data:                  data,
+			DataTransferDirection: DataTransferFromDevice,
+		}); err != nil {
+			return nil, fmt.Errorf("error during LOG SENSE: %w", err)
+		}
+		if data[0]&0b111111 != r.PageCode {
+			return nil, fmt.Errorf("requested log page %x, got %x", r.PageCode, data[1])
+		}
+		if data[1] != r.SubpageCode {
+			return nil, fmt.Errorf("requested log subpage %x, got %x", r.SubpageCode, data[1])
+		}
+		pageLength := binary.BigEndian.Uint16(data[2:4])
+		if pageLength > math.MaxUint16-4 {
+			// Guard against uint16 overflows, this cannot be requested anyways
+			return nil, fmt.Errorf("device log page is too long (%d bytes)", pageLength)
+		}
+		if pageLength > uint16(len(data)-4) {
+			bufferSize = pageLength + 4
+			continue
+		}
+		return data[4 : pageLength+4], nil
+	}
+}
+
+// SupportedLogPages returns a map with all supported log pages.
+// This can return an error if the device does not support logs at all.
+func (d *Device) SupportedLogPages() (map[uint8]bool, error) {
+	raw, err := d.LogSenseRaw(LogSenseRequest{PageCode: 0})
+	if err != nil {
+		return nil, err
+	}
+	res := make(map[uint8]bool)
+	for _, r := range raw {
+		res[r] = true
+	}
+	return res, nil
+}
+
+// PageAndSubpage identifies a log page uniquely.
+type PageAndSubpage uint16
+
+func NewPageAndSubpage(page, subpage uint8) PageAndSubpage {
+	return PageAndSubpage(uint16(page)<<8 | uint16(subpage))
+}
+
+func (p PageAndSubpage) Page() uint8 {
+	return uint8(p >> 8)
+}
+func (p PageAndSubpage) Subpage() uint8 {
+	return uint8(p & 0xFF)
+}
+
+func (p PageAndSubpage) String() string {
+	return fmt.Sprintf("Page %xh Subpage %xh", p.Page(), p.Subpage())
+}
+
+// SupportedLogPagesAndSubpages returns the list of supported pages and subpages.
+// This can return an error if the device does not support logs at all.
+func (d *Device) SupportedLogPagesAndSubpages() (map[PageAndSubpage]bool, error) {
+	raw, err := d.LogSenseRaw(LogSenseRequest{PageCode: 0x00, SubpageCode: 0xff})
+	if err != nil {
+		return nil, err
+	}
+	res := make(map[PageAndSubpage]bool)
+	for i := 0; i < len(raw)/2; i++ {
+		res[NewPageAndSubpage(raw[i*2], raw[(i*2)+1])] = true
+	}
+	return res, nil
+}
+
+// SupportedLogSubPages returns the list of subpages supported in a log page.
+func (d *Device) SupportedLogSubPages(pageCode uint8) (map[uint8]bool, error) {
+	raw, err := d.LogSenseRaw(LogSenseRequest{PageCode: pageCode, SubpageCode: 0xff})
+	if err != nil {
+		return nil, err
+	}
+	res := make(map[uint8]bool)
+	for _, r := range raw {
+		res[r] = true
+	}
+	return res, nil
+}
+
+type LogParameter struct {
+	// DisableUpdate indicates if the device is updating this parameter.
+	// If this is true the parameter has either overflown or updating has been
+	// manually disabled.
+	DisableUpdate bool
+	// TargetSaveDisable indicates if automatic saving of this parameter has
+	// been disabled.
+	TargetSaveDisable bool
+	// FormatAndLinking contains the format of the log parameter.
+	FormatAndLinking uint8
+	// Data contains the payload of the log parameter.
+	Data []byte
+}
+
+// LogSenseParameters returns the parameters of a log page. The returned map
+// contains one entry per parameter ID in the result. The order of parameters
+// of the same ID is kept.
+func (d *Device) LogSenseParameters(r LogSenseRequest) (map[uint16][]LogParameter, error) {
+	raw, err := d.LogSenseRaw(r)
+	if err != nil {
+		return nil, err
+	}
+	res := make(map[uint16][]LogParameter)
+	for {
+		if len(raw) == 0 {
+			break
+		}
+		if len(raw) < 4 {
+			return nil, errors.New("not enough data left to read full parameter metadata")
+		}
+		var param LogParameter
+		parameterCode := binary.BigEndian.Uint16(raw[0:2])
+		param.DisableUpdate = raw[2]&(1<<7) != 0
+		param.TargetSaveDisable = raw[2]&(1<<5) != 0
+		param.FormatAndLinking = raw[2] & 0b11
+		if int(raw[3]) > len(raw)-4 {
+			fmt.Println(raw[3], len(raw))
+			return nil, errors.New("unable to read parameter, not enough data for indicated length")
+		}
+		param.Data = raw[4 : int(raw[3])+4]
+		raw = raw[int(raw[3])+4:]
+		res[parameterCode] = append(res[parameterCode], param)
+	}
+	return res, nil
+}