diff --git a/oem/smc/drive.go b/oem/smc/drive.go new file mode 100644 index 00000000..dd2f20fc --- /dev/null +++ b/oem/smc/drive.go @@ -0,0 +1,83 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package smc + +import ( + "encoding/json" + "errors" + + "github.com/stmcginnis/gofish/common" + "github.com/stmcginnis/gofish/redfish" +) + +// ErrActionNotSupported is returned when the requested OEM-specific action +// does not appear to be supported. This might happen when a device is new +// or upgraded to a new firmware that follows the DMTF standards. +var ErrActionNotSupported = errors.New("oem-specific action unsupported") + +// Drive extends a redfish.Drive for additional OEM fields +type Drive struct { + redfish.Drive + + // Fields from the SMC OEM section + Temperature int + PercentageDriveLifeUsed int + DriveFunctional bool + + // indicateTarget is the uri to hit to change the light state + indicateTarget string +} + +// FromDrive returns an OEM-extended redfish drive +func FromDrive(drive *redfish.Drive) (Drive, error) { + smcDrive := Drive{ + Drive: *drive, + } + + var t struct { + Oem struct { + Supermicro struct { + Temperature int + PercentageDriveLifeUsed int + DriveFunctional bool + } `json:"Supermicro"` + } `json:"Oem"` + Actions struct { + Oem struct { + DriveIndicate common.ActionTarget `json:"#Drive.Indicate"` + SmcDriveIndicate common.ActionTarget `json:"#SmcDrive.Indicate"` + } `json:"Oem"` + } `json:"Actions"` + } + + // Populate the Oem data + if err := json.Unmarshal(drive.RawData, &t); err != nil { + return smcDrive, err + } + + smcDrive.Temperature = t.Oem.Supermicro.Temperature + smcDrive.PercentageDriveLifeUsed = t.Oem.Supermicro.PercentageDriveLifeUsed + smcDrive.DriveFunctional = t.Oem.Supermicro.DriveFunctional + + // We check both the SmcDriveIndicate and the DriveIndicate targets + // in the Oem sections - certain models and bmc firmwares will mix + // these up, so we check both + smcDrive.indicateTarget = t.Actions.Oem.DriveIndicate.Target + if t.Actions.Oem.SmcDriveIndicate.Target != "" { + smcDrive.indicateTarget = t.Actions.Oem.SmcDriveIndicate.Target + } + + return smcDrive, nil +} + +// Indicate will set the indicator light activity, true for on, false for off +func (d *Drive) Indicate(active bool) error { + // Return a common error to let the user try falling back on the normal gofish path + if d.indicateTarget == "" { + return ErrActionNotSupported + } + + return d.Post(d.indicateTarget, map[string]interface{}{"Active": active}) +} diff --git a/oem/smc/drive_test.go b/oem/smc/drive_test.go new file mode 100644 index 00000000..f61a4793 --- /dev/null +++ b/oem/smc/drive_test.go @@ -0,0 +1,71 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package smc + +import ( + "encoding/json" + "testing" + + "github.com/stmcginnis/gofish/redfish" +) + +var smcDriveBody = `{ + "@odata.type": "#Drive.v1_6_2.Drive", + "@odata.id": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives/Disk.Bay.22", + "Name": "Disk.Bay.22", + "Id": "22", + "Manufacturer": "INTEL", + "SerialNumber": "PHLWOOFMEOWIAMCATDOG", + "Model": "INTEL SSDPE2KX080T8O", + "StatusIndicator": "OK", + "FailurePredicted": false, + "CapacityBytes": 8001563222016, + "CapableSpeedGbs": 31.5, + "Oem": { + "Supermicro": { + "@odata.type": "#SmcDriveExtensions.v1_0_0.Drive", + "Temperature": 33, + "PercentageDriveLifeUsed": 3, + "DriveFunctional": true + } + }, + "IndicatorLED": "Off", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Links": { + "Volumes": [] + }, + "Actions": { + "Oem": { + "#Drive.Indicate": { + "target": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives/Disk.Bay.22/Actions/Oem/Drive.Indicate", + "@Redfish.ActionInfo": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives/Disk.Bay.22/IndicateActionInfo" + } + } + } +}` + +// TestSmcDriveOem tests the parsing of the Drive oem field +func TestSmcDriveOem(t *testing.T) { + drive := &redfish.Drive{} + if err := json.Unmarshal([]byte(smcDriveBody), drive); err != nil { + t.Fatalf("error decoding json: %v", err) + } + + smcDrive, err := FromDrive(drive) + if err != nil { + t.Fatalf("error getting oem info from drive: %v", err) + } + + if smcDrive.Temperature != 33 { + t.Errorf("unexpected oem drive temperature: %d", smcDrive.Temperature) + } + + if smcDrive.indicateTarget != "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives/Disk.Bay.22/Actions/Oem/Drive.Indicate" { + t.Errorf("unexpected oem drive indicator target: %s", smcDrive.indicateTarget) + } +} diff --git a/redfish/drive.go b/redfish/drive.go index 79eace6a..a90b92a0 100644 --- a/redfish/drive.go +++ b/redfish/drive.go @@ -299,6 +299,8 @@ type Drive struct { // WriteCacheEnabled shall indicate whether the drive // write cache is enabled. WriteCacheEnabled bool + // Oem is all the available OEM information for the drive + Oem json.RawMessage // ActiveSoftwareImage shall contain a link a resource of type SoftwareInventory that represents the active drive // firmware image. @@ -331,8 +333,9 @@ type Drive struct { StoragePoolsCount int // secureEraseTarget is the URL for SecureErase actions. secureEraseTarget string - // rawData holds the original serialized JSON so we can compare updates. - rawData []byte + // RawData holds the original serialized JSON so we can compare updates + // as well as access Oem values in the oem package. + RawData []byte } // UnmarshalJSON unmarshals a Drive object from the raw JSON. @@ -391,6 +394,7 @@ func (drive *Drive) UnmarshalJSON(b []byte) error { drive.EndpointsCount = t.Links.EndpointCount drive.networkDeviceFunctions = t.Links.NetworkDeviceFunctions.ToStrings() drive.NetworkDeviceFunctionsCount = t.Links.NetworkDeviceFunctionsCount + drive.Oem = t.Oem drive.pcieFunctions = t.Links.PCIeFunctions.ToStrings() drive.PCIeFunctionCount = t.Links.PCIeFunctionsCount drive.softwareImages = t.Links.SoftwareImages.ToStrings() @@ -404,7 +408,7 @@ func (drive *Drive) UnmarshalJSON(b []byte) error { drive.secureEraseTarget = t.Actions.SecureErase.Target // This is a read/write object, so we need to save the raw object data for later - drive.rawData = b + drive.RawData = b return nil } @@ -414,7 +418,7 @@ func (drive *Drive) Update() error { // Get a representation of the object's original state so we can find what // to update. original := new(Drive) - err := original.UnmarshalJSON(drive.rawData) + err := original.UnmarshalJSON(drive.RawData) if err != nil { return err }