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
+}