diff --git a/bmc/connection.go b/bmc/connection.go index b1bf9489..a038b186 100644 --- a/bmc/connection.go +++ b/bmc/connection.go @@ -8,6 +8,10 @@ import ( "github.com/hashicorp/go-multierror" ) +var ( + ErrOpenConnection = errors.New("error opening connection") +) + // Opener interface for opening a connection to a BMC type Opener interface { Open(ctx context.Context) error diff --git a/bmc/sensors.go b/bmc/sensors.go new file mode 100644 index 00000000..ce7e6367 --- /dev/null +++ b/bmc/sensors.go @@ -0,0 +1,210 @@ +package bmc + +import ( + "context" + "errors" + "fmt" + + "github.com/bmc-toolbox/bmclib/devices" + "github.com/hashicorp/go-multierror" +) + +// PowerSensorsGetter retrieves power consumption values +type PowerSensorsGetter interface { + PowerSensors(ctx context.Context) ([]*devices.PowerSensor, error) +} + +// TemperatureSensorGetter retrieves temperature values +type TemperatureSensorsGetter interface { + TemperatureSensors(ctx context.Context) ([]*devices.TemperatureSensor, error) +} + +// FanSensorsGetter retrieves fan speed data +type FanSensorsGetter interface { + FanSensors(ctx context.Context) ([]*devices.FanSensor, error) +} + +// ChassisHealthGetter retrieves chassis health data +type ChassisHealthGetter interface { + ChassisHealth(ctx context.Context) ([]*devices.ChassisHealth, error) +} + +// PowerGetter interface implementation identifier and passthrough methods + +// GetPowerSensors returns power draw data, trying all interface implementations passed in +func GetPowerSensors(ctx context.Context, p []PowerSensorsGetter) (power []*devices.PowerSensor, err error) { +Loop: + for _, elem := range p { + if elem == nil { + continue + } + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + break Loop + default: + power, vErr := elem.PowerSensors(ctx) + if vErr != nil { + err = multierror.Append(err, vErr) + continue + } + return power, nil + } + } + + return power, multierror.Append(err, errors.New("failed to get power sensor data")) +} + +// GetPowerSensorsFromInterfaces identfies implementations of the PowerSensorGetter interface and acts as a pass through method +func GetPowerSensorsFromInterfaces(ctx context.Context, generic []interface{}) (power []*devices.PowerSensor, err error) { + powerDrawGetter := make([]PowerSensorsGetter, 0) + for _, elem := range generic { + switch p := elem.(type) { + case PowerSensorsGetter: + powerDrawGetter = append(powerDrawGetter, p) + default: + e := fmt.Sprintf("not a PowerSensorGetter implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(powerDrawGetter) == 0 { + return power, multierror.Append(err, errors.New("no PowerSensorGetter implementations found")) + } + + return GetPowerSensors(ctx, powerDrawGetter) +} + +// TemperatureSensorsGetter interface identifier and passthrough methods + +// GetTemperatureSensors returns temperature data, trying all interface implementations passed in +func GetTemperatureSensors(ctx context.Context, p []TemperatureSensorsGetter) (temps []*devices.TemperatureSensor, err error) { +Loop: + for _, elem := range p { + if elem == nil { + continue + } + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + break Loop + default: + temps, vErr := elem.TemperatureSensors(ctx) + if vErr != nil { + err = multierror.Append(err, vErr) + continue + } + return temps, nil + } + } + + return temps, multierror.Append(err, errors.New("failed to get temperature sensor data")) +} + +// GetTemperatureSensorsFromInterfaces identfies implementations of the TemperatureGetter interface and acts as a pass through method +func GetTemperatureSensorsFromInterfaces(ctx context.Context, generic []interface{}) (temps []*devices.TemperatureSensor, err error) { + gets := make([]TemperatureSensorsGetter, 0) + for _, elem := range generic { + switch p := elem.(type) { + case TemperatureSensorsGetter: + gets = append(gets, p) + default: + e := fmt.Sprintf("not a TemperatureGetter implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(gets) == 0 { + return temps, multierror.Append(err, errors.New("no TemperatureGetter implementations found")) + } + + return GetTemperatureSensors(ctx, gets) +} + +// FanSensorsGetter interface identifier and passthrough methods + +// GetFanSensors returns fan speed data, trying all interface implementations passed in +func GetFanSensors(ctx context.Context, p []FanSensorsGetter) (fanSensors []*devices.FanSensor, err error) { +Loop: + for _, elem := range p { + if elem == nil { + continue + } + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + break Loop + default: + fanSensors, vErr := elem.FanSensors(ctx) + if vErr != nil { + err = multierror.Append(err, vErr) + continue + } + return fanSensors, nil + } + } + + return fanSensors, multierror.Append(err, errors.New("failed to get fan sensor data")) +} + +// GetFanSensorsFromInterfaces identfies implementations of the FanSpeedGetter interface and acts as a pass through method +func GetFanSensorsFromInterfaces(ctx context.Context, generic []interface{}) (fanSensors []*devices.FanSensor, err error) { + gets := make([]FanSensorsGetter, 0) + for _, elem := range generic { + switch p := elem.(type) { + case FanSensorsGetter: + gets = append(gets, p) + default: + e := fmt.Sprintf("not a FanSensorGetter implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(gets) == 0 { + return fanSensors, multierror.Append(err, errors.New("no FanSensorGetter implementations found")) + } + + return GetFanSensors(ctx, gets) +} + +// ChassisHealthGetter interface identifier and passthrough methods + +// GetChassisHealth gets all chassis health data, trying all interface implementations passed in +func GetChassisHealth(ctx context.Context, p []ChassisHealthGetter) (health []*devices.ChassisHealth, err error) { +Loop: + for _, elem := range p { + if elem == nil { + continue + } + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + break Loop + default: + health, vErr := elem.ChassisHealth(ctx) + if vErr != nil { + err = multierror.Append(err, vErr) + continue + } + return health, nil + } + } + + return health, multierror.Append(err, errors.New("failed to get chassis health")) +} + +// GetChassisHealthFromInterfaces identfies implementations of the ChassisHealthGetter interface and acts as a pass through method +func GetChassisHealthFromInterfaces(ctx context.Context, generic []interface{}) (health []*devices.ChassisHealth, err error) { + gets := make([]ChassisHealthGetter, 0) + for _, elem := range generic { + switch p := elem.(type) { + case ChassisHealthGetter: + gets = append(gets, p) + default: + e := fmt.Sprintf("not a ChassisHealthGetter implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(gets) == 0 { + return health, multierror.Append(err, errors.New("no FanSensorGetter implementations found")) + } + + return GetChassisHealth(ctx, gets) +} diff --git a/client.go b/client.go index 477a7e55..1f76ae53 100644 --- a/client.go +++ b/client.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/bmc-toolbox/bmclib/bmc" + "github.com/bmc-toolbox/bmclib/devices" "github.com/bmc-toolbox/bmclib/providers/asrockrack" "github.com/bmc-toolbox/bmclib/providers/dell/idrac9" "github.com/bmc-toolbox/bmclib/providers/goipmi" @@ -221,3 +222,23 @@ func (c *Client) GetBIOSVersion(ctx context.Context) (version string, err error) func (c *Client) UpdateBIOSFirmware(ctx context.Context, fileReader io.Reader, fileSize int64) (err error) { return bmc.UpdateBIOSFirmwareFromInterfaces(ctx, fileReader, fileSize, c.Registry.GetDriverInterfaces()) } + +// GetPowerSensors pass through library function +func (c *Client) GetPowerSensors(ctx context.Context) (sensors []*devices.PowerSensor, err error) { + return bmc.GetPowerSensorsFromInterfaces(ctx, c.Registry.GetDriverInterfaces()) +} + +// GetTemperatureSensors pass through library function +func (c *Client) GetTemperatureSensors(ctx context.Context) (sensors []*devices.TemperatureSensor, err error) { + return bmc.GetTemperatureSensorsFromInterfaces(ctx, c.Registry.GetDriverInterfaces()) +} + +// GetFanSensors pass through library function +func (c *Client) GetFanSensors(ctx context.Context) (sensors []*devices.FanSensor, err error) { + return bmc.GetFanSensorsFromInterfaces(ctx, c.Registry.GetDriverInterfaces()) +} + +// GetChassisHealth pass through library function +func (c *Client) GetChassisHealth(ctx context.Context) (health []*devices.ChassisHealth, err error) { + return bmc.GetChassisHealthFromInterfaces(ctx, c.Registry.GetDriverInterfaces()) +} diff --git a/devices/sensors.go b/devices/sensors.go new file mode 100644 index 00000000..d23d17d0 --- /dev/null +++ b/devices/sensors.go @@ -0,0 +1,30 @@ +package devices + +type PowerSensor struct { + ID string + Name string + InputWatts float32 + OutputWatts float32 + LastOutputWatts float32 +} + +type TemperatureSensor struct { + ID string + Name string + ReadingCelsius float32 + PhysicalContext string +} + +type FanSensor struct { + ID string + Name string + Reading float32 + PhysicalContext string +} + +type ChassisHealth struct { + ID string + Name string + State string + Health string +} diff --git a/examples/v1/sensors/main.go b/examples/v1/sensors/main.go new file mode 100644 index 00000000..bb955e70 --- /dev/null +++ b/examples/v1/sensors/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/bmc-toolbox/bmclib" + "github.com/bombsimon/logrusr" + "github.com/sirupsen/logrus" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + host := "" + port := "" + user := "" + pass := "" + + l := logrus.New() + l.Level = logrus.DebugLevel + logger := logrusr.NewLogger(l) + + var err error + + cl := bmclib.NewClient(host, port, user, pass, bmclib.WithLogger(logger)) + cl.Registry.Drivers = cl.Registry.Using("redfish") + err = cl.Open(ctx) + if err != nil { + log.Fatal(err, "bmc login failed") + } + + p, err := cl.GetPowerSensors(ctx) + if err != nil { + fmt.Println(err, "unable to retrieve power sensor data") + } + + fmt.Printf("%+v\n", p) + + t, err := cl.GetTemperatureSensors(ctx) + if err != nil { + fmt.Println(err, "unable to retrieve temperature sensor data") + } + + fmt.Printf("%+v\n", t) + + f, err := cl.GetFanSensors(ctx) + if err != nil { + fmt.Println(err, "unable to retrieve fan sensor data") + } + + fmt.Printf("%+v\n", f) + + c, err := cl.GetChassisHealth(ctx) + if err != nil { + fmt.Println(err, "unable to retrieve chassis health data") + } + + fmt.Printf("%+v\n", c) + +} diff --git a/providers/providers.go b/providers/providers.go index 887fa01a..46864146 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -29,4 +29,12 @@ const ( FeatureBmcFirmwareUpdate registrar.Feature = "bmcfirwareupdate" // FeatureBiosFirmwareUpdate means an implementation that updates the BIOS firmware FeatureBiosFirmwareUpdate registrar.Feature = "biosfirwareupdate" + // FeaturePowersensors indicates an implementation that returns Power sensor information + FeaturePowerSensors registrar.Feature = "powersensors" + // FeatureTemperatureSensors indicates an implementation that returns Temperature sensor information + FeatureTemperatureSensors registrar.Feature = "temperaturesensors" + // FeatureFanSensors indicates an implementation that returns Fan sensor information + FeatureFanSensors registrar.Feature = "fansensors" + // FeatureChassisHealth indicates an implementation that returns ChassisHealth information + FeatureChassisHealth registrar.Feature = "chassishealth" ) diff --git a/providers/redfish/redfish.go b/providers/redfish/redfish.go index a1b453eb..7e29aafa 100644 --- a/providers/redfish/redfish.go +++ b/providers/redfish/redfish.go @@ -6,7 +6,9 @@ import ( "strings" "time" + bmcErrs "github.com/bmc-toolbox/bmclib/errors" "github.com/bmc-toolbox/bmclib/providers" + "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" "github.com/pkg/errors" @@ -26,17 +28,35 @@ var ( Features = registrar.Features{ providers.FeaturePowerSet, providers.FeaturePowerState, + providers.FeaturePowerSensors, + providers.FeatureTemperatureSensors, + providers.FeatureFanSensors, + providers.FeatureChassisHealth, + } + + // Supported Chassis Odata IDs + chassisOdataIdURLs = []string{ + // Dells + "/redfish/v1/Chassis/System.Embedded.1", + // Supermicro + "/redfish/v1/Chassis/1", + // MegaRAC + "/redfish/v1/Chassis/Self", } + + ErrRedfishChassisOdataID = errors.New("no compatible chassis Odata IDs identified") + ErrRedfishServiceNil = errors.New("redfish connection returned a nil redfish Service object") ) // Conn details for redfish client type Conn struct { - Host string - Port string - User string - Pass string - conn *gofish.APIClient - Log logr.Logger + Host string + Port string + User string + Pass string + Vendor string + conn *gofish.APIClient + Log logr.Logger } // Open a connection to a BMC via redfish @@ -50,7 +70,7 @@ func (c *Conn) Open(ctx context.Context) (err error) { c.conn, err = gofish.ConnectContext(ctx, config) if err != nil { - return err + return errors.Wrap(bmcErrs.ErrLoginFailed, err.Error()) } return nil } diff --git a/providers/redfish/sensors.go b/providers/redfish/sensors.go new file mode 100644 index 00000000..f26cc0d0 --- /dev/null +++ b/providers/redfish/sensors.go @@ -0,0 +1,210 @@ +package redfish + +import ( + "context" + "fmt" + "strconv" + + "github.com/bmc-toolbox/bmclib/devices" +) + +func chassisCompatible(chassisOdataID string) bool { + for _, url := range chassisOdataIdURLs { + if url == chassisOdataID { + return true + } + } + + return false +} + +func (c *Conn) PowerSensors(ctx context.Context) ([]*devices.PowerSensor, error) { + + data := make([]*devices.PowerSensor, 0) + + service := c.conn.Service + if service == nil { + return nil, ErrRedfishServiceNil + } + + chassis, err := service.Chassis() + if err != nil { + return nil, err + } + + compatible := 0 + for _, ch := range chassis { + fmt.Println(ch.ODataID) + if !chassisCompatible(ch.ODataID) { + continue + } + + compatible++ + p, err := ch.Power() + if err != nil { + return nil, err + } + + for idx, supply := range p.PowerSupplies { + id := p.ID + + if p.ID == "" { + id = strconv.Itoa(idx) + } + + c := &devices.PowerSensor{ + Name: supply.Name, + ID: id, + InputWatts: supply.PowerInputWatts, + OutputWatts: supply.PowerOutputWatts, + LastOutputWatts: supply.LastPowerOutputWatts, + } + data = append(data, c) + } + + } + + if compatible == 0 { + return nil, ErrRedfishChassisOdataID + } + + return data, nil +} + +func (c *Conn) TemperatureSensors(ctx context.Context) ([]*devices.TemperatureSensor, error) { + + data := make([]*devices.TemperatureSensor, 0) + + service := c.conn.Service + if service == nil { + return nil, ErrRedfishServiceNil + } + + chassis, err := service.Chassis() + if err != nil { + return nil, err + } + + compatible := 0 + for _, ch := range chassis { + if !chassisCompatible(ch.ODataID) { + continue + } + + compatible++ + p, err := ch.Thermal() + if err != nil { + return nil, err + } + + for idx, s := range p.Temperatures { + id := s.ID + + if s.ID == "" { + id = strconv.Itoa(idx) + } + + t := &devices.TemperatureSensor{ + ID: id, + Name: s.Name, + PhysicalContext: s.PhysicalContext, + ReadingCelsius: s.ReadingCelsius, + } + + data = append(data, t) + } + } + + if compatible == 0 { + return nil, ErrRedfishChassisOdataID + } + + return data, nil +} + +func (c *Conn) FanSensors(ctx context.Context) ([]*devices.FanSensor, error) { + data := make([]*devices.FanSensor, 0) + + service := c.conn.Service + if service == nil { + return nil, ErrRedfishServiceNil + } + + chassis, err := service.Chassis() + if err != nil { + return nil, err + } + + compatible := 0 + for _, ch := range chassis { + if !chassisCompatible(ch.ODataID) { + continue + } + + compatible++ + p, err := ch.Thermal() + if err != nil { + return nil, err + } + + for idx, s := range p.Fans { + id := s.ID + + if s.ID == "" { + id = strconv.Itoa(idx) + } + + t := &devices.FanSensor{ + ID: id, + Name: s.Name, + PhysicalContext: s.PhysicalContext, + Reading: s.Reading, + } + + data = append(data, t) + } + } + + if compatible == 0 { + return nil, ErrRedfishChassisOdataID + } + + return data, nil +} + +func (c *Conn) ChassisHealth(ctx context.Context) ([]*devices.ChassisHealth, error) { + + data := make([]*devices.ChassisHealth, 0) + + service := c.conn.Service + if service == nil { + return nil, ErrRedfishServiceNil + } + + chassis, err := service.Chassis() + if err != nil { + return nil, err + } + + compatible := 0 + for _, ch := range chassis { + if !chassisCompatible(ch.ODataID) { + continue + } + + compatible++ + h := &devices.ChassisHealth{ + ID: ch.ID, + Name: ch.Name, + State: string(ch.Status.State), + Health: string(ch.Status.Health), + } + data = append(data, h) + } + + if compatible == 0 { + return nil, ErrRedfishChassisOdataID + } + + return data, nil +}