Skip to content

Commit

Permalink
Add support for sending NMI (#384)
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Mar 11, 2024
2 parents 7a00485 + 8dc2d91 commit 5b958a7
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 1 deletion.
66 changes: 66 additions & 0 deletions bmc/nmi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package bmc

import (
"context"
"errors"
"fmt"
"time"

"github.com/hashicorp/go-multierror"
)

type NMISender interface {
SendNMI(ctx context.Context) error
}

func sendNMI(ctx context.Context, timeout time.Duration, sender NMISender, metadata *Metadata) error {
senderName := getProviderName(sender)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, senderName)

err := sender.SendNMI(ctx)
if err != nil {
metadata.FailedProviderDetail[senderName] = err.Error()
return err
}

metadata.SuccessfulProvider = senderName

return nil
}

// SendNMIFromInterface will look for providers that implement NMISender
// and attempt to call SendNMI until a provider is successful,
// or all providers have been exhausted.
func SendNMIFromInterface(
ctx context.Context,
timeout time.Duration,
providers []interface{},
) (metadata Metadata, err error) {
metadata = newMetadata()

for _, provider := range providers {
sender, ok := provider.(NMISender)
if !ok {
err = multierror.Append(err, fmt.Errorf("not an NMISender implementation: %T", provider))
continue
}

sendNMIErr := sendNMI(ctx, timeout, sender, &metadata)
if sendNMIErr != nil {
err = multierror.Append(err, sendNMIErr)
continue
}
return metadata, nil
}

if len(metadata.ProvidersAttempted) == 0 {
err = multierror.Append(err, errors.New("no NMISender implementations found"))
} else {
err = multierror.Append(err, errors.New("failed to send NMI"))
}

return metadata, err
}
124 changes: 124 additions & 0 deletions bmc/nmi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package bmc

import (
"context"
"testing"
"time"

"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)

type mockNMISender struct {
err error
}

func (m *mockNMISender) SendNMI(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return m.err
}
}

func (m *mockNMISender) Name() string {
return "mock"
}

func TestSendNMIFromInterface(t *testing.T) {
testCases := []struct {
name string
mockSenders []interface{}
errMsg string
isTimedout bool
expectedMetadata Metadata
}{
{
name: "success",
mockSenders: []interface{}{&mockNMISender{}},
expectedMetadata: Metadata{
SuccessfulProvider: "mock",
ProvidersAttempted: []string{"mock"},
FailedProviderDetail: make(map[string]string),
},
},
{
name: "success with multiple senders",
mockSenders: []interface{}{
nil,
"foo",
&mockNMISender{err: errors.New("err from sender")},
&mockNMISender{},
},
expectedMetadata: Metadata{
SuccessfulProvider: "mock",
ProvidersAttempted: []string{"mock", "mock"},
FailedProviderDetail: map[string]string{"mock": "err from sender"},
},
},
{
name: "not an nmisender",
mockSenders: []interface{}{nil},
errMsg: "not an NMISender",
expectedMetadata: Metadata{
FailedProviderDetail: make(map[string]string),
},
},
{
name: "no nmisenders",
mockSenders: []interface{}{},
errMsg: "no NMISender implementations found",
expectedMetadata: Metadata{
FailedProviderDetail: make(map[string]string),
},
},
{
name: "timed out",
mockSenders: []interface{}{&mockNMISender{}},
isTimedout: true,
errMsg: "context deadline exceeded",
expectedMetadata: Metadata{
ProvidersAttempted: []string{"mock"},
FailedProviderDetail: map[string]string{"mock": "context deadline exceeded"},
},
},
{
name: "error from nmisender",
mockSenders: []interface{}{&mockNMISender{err: errors.New("foobar")}},
errMsg: "foobar",
expectedMetadata: Metadata{
ProvidersAttempted: []string{"mock"},
FailedProviderDetail: map[string]string{"mock": "foobar"},
},
},
{
name: "error when fail to send",
mockSenders: []interface{}{&mockNMISender{err: errors.New("err from sender")}},
errMsg: "failed to send NMI",
expectedMetadata: Metadata{
ProvidersAttempted: []string{"mock"},
FailedProviderDetail: map[string]string{"mock": "err from sender"},
},
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
timeout := time.Second * 60
if tt.isTimedout {
timeout = 0
}

metadata, err := SendNMIFromInterface(context.Background(), timeout, tt.mockSenders)

if tt.errMsg == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.errMsg)
}

assert.Equal(t, tt.expectedMetadata, metadata)
})
}
}
11 changes: 11 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,3 +717,14 @@ func (c *Client) GetSystemEventLogRaw(ctx context.Context) (eventlog string, err
c.setMetadata(metadata)
return eventlog, err
}

// SendNMI tells the BMC to issue an NMI to the device
func (c *Client) SendNMI(ctx context.Context) error {
ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "SendNMI")
defer span.End()

metadata, err := bmc.SendNMIFromInterface(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces())
c.setMetadata(metadata)

return err
}
10 changes: 10 additions & 0 deletions internal/ipmi/ipmi.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,13 @@ func (i *Ipmi) DeactivateSOL(ctx context.Context) (err error) {
}
return err
}

// SendPowerDiag tells the BMC to issue an NMI to the device
func (i *Ipmi) SendPowerDiag(ctx context.Context) error {
_, err := i.run(ctx, []string{"chassis", "power", "diag"})
if err != nil {
err = errors.Wrap(err, "failed sending power diag")
}

return err
}
21 changes: 20 additions & 1 deletion internal/redfishwrapper/power.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ func (c *Client) SystemForceOff(ctx context.Context) (ok bool, err error) {

system.DisableEtagMatch(c.disableEtagMatch)


err = system.Reset(rf.ForceOffResetType)
if err != nil {
return false, err
Expand All @@ -221,3 +220,23 @@ func (c *Client) SystemForceOff(ctx context.Context) (ok bool, err error) {

return true, nil
}

// SendNMI tells the BMC to issue an NMI to the device
func (c *Client) SendNMI(_ context.Context) error {
if err := c.SessionActive(); err != nil {
return errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error())
}

ss, err := c.client.Service.Systems()
if err != nil {
return err
}

for _, system := range ss {
if err = system.Reset(rf.NmiResetType); err != nil {
return err
}
}

return nil
}
5 changes: 5 additions & 0 deletions providers/dell/idrac.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ func (c *Conn) BmcReset(ctx context.Context, resetType string) (ok bool, err err
return c.redfishwrapper.BMCReset(ctx, resetType)
}

// SendNMI tells the BMC to issue an NMI to the device
func (c *Conn) SendNMI(ctx context.Context) error {
return c.redfishwrapper.SendNMI(ctx)
}

// deviceManufacturer returns the device manufacturer and model attributes
func (c *Conn) deviceManufacturer(ctx context.Context) (vendor string, err error) {
systems, err := c.redfishwrapper.Systems()
Expand Down
5 changes: 5 additions & 0 deletions providers/ipmitool/ipmitool.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,8 @@ func (c *Conn) GetSystemEventLog(ctx context.Context) (entries [][]string, err e
func (c *Conn) GetSystemEventLogRaw(ctx context.Context) (eventlog string, err error) {
return c.ipmitool.GetSystemEventLogRaw(ctx)
}

// SendNMI tells the BMC to issue an NMI to the device
func (c *Conn) SendNMI(ctx context.Context) error {
return c.ipmitool.SendPowerDiag(ctx)
}
18 changes: 18 additions & 0 deletions providers/ipmitool/ipmitool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,21 @@ func TestSystemEventLogGetRaw(t *testing.T) {
t.Log(eventlog)
t.Fatal()
}

func TestSendNMI(t *testing.T) {
t.Skip("need real ipmi server")
host := "127.0.0.1"
port := "623"
user := "ADMIN"
pass := "ADMIN"
i, err := New(host, user, pass, WithPort(port), WithLogger(logging.DefaultLogger()))
if err != nil {
t.Fatal(err)
}
err = i.SendNMI(context.Background())
if err != nil {
t.Fatal(err)
}
t.Log("NMI sent")
t.Fatal()
}
5 changes: 5 additions & 0 deletions providers/openbmc/openbmc.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,8 @@ func (c *Conn) Inventory(ctx context.Context) (device *common.Device, err error)
func (c *Conn) BmcReset(ctx context.Context, resetType string) (ok bool, err error) {
return c.redfishwrapper.BMCReset(ctx, resetType)
}

// SendNMI tells the BMC to issue an NMI to the device
func (c *Conn) SendNMI(ctx context.Context) error {
return c.redfishwrapper.SendNMI(ctx)
}
5 changes: 5 additions & 0 deletions providers/redfish/redfish.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,8 @@ func (c *Conn) Inventory(ctx context.Context) (device *common.Device, err error)
func (c *Conn) GetBiosConfiguration(ctx context.Context) (biosConfig map[string]string, err error) {
return c.redfishwrapper.GetBiosConfiguration(ctx)
}

// SendNMI tells the BMC to issue an NMI to the device
func (c *Conn) SendNMI(ctx context.Context) error {
return c.redfishwrapper.SendNMI(ctx)
}
5 changes: 5 additions & 0 deletions providers/supermicro/supermicro.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,3 +544,8 @@ func hostIP(hostURL string) (string, error) {

return hostURLParsed.Host, nil
}

// SendNMI tells the BMC to issue an NMI to the device
func (c *Client) SendNMI(ctx context.Context) error {
return c.serviceClient.redfish.SendNMI(ctx)
}

0 comments on commit 5b958a7

Please sign in to comment.