Skip to content

Commit

Permalink
Add support for sending NMI
Browse files Browse the repository at this point in the history
Support for sending an NMI has been added to ipmi, redfish,
redfishwrapper, and all providers that use the redfishwrapper.
  • Loading branch information
coffeefreak101 committed Mar 8, 2024
1 parent 7a00485 commit 41bb750
Show file tree
Hide file tree
Showing 11 changed files with 270 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()

Check warning on line 724 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L722-L724

Added lines #L722 - L724 were not covered by tests

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

Check warning on line 727 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L726-L727

Added lines #L726 - L727 were not covered by tests

return err

Check warning on line 729 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L729

Added line #L729 was not covered by tests
}
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
}
17 changes: 16 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,19 @@ 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 {
ss, err := c.client.Service.Systems()
if err != nil {
return err

Check warning on line 228 in internal/redfishwrapper/power.go

View check run for this annotation

Codecov / codecov/patch

internal/redfishwrapper/power.go#L225-L228

Added lines #L225 - L228 were not covered by tests
}

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

Check warning on line 233 in internal/redfishwrapper/power.go

View check run for this annotation

Codecov / codecov/patch

internal/redfishwrapper/power.go#L231-L233

Added lines #L231 - L233 were not covered by tests
}
}

return nil

Check warning on line 237 in internal/redfishwrapper/power.go

View check run for this annotation

Codecov / codecov/patch

internal/redfishwrapper/power.go#L237

Added line #L237 was not covered by tests
}
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)

Check warning on line 224 in providers/dell/idrac.go

View check run for this annotation

Codecov / codecov/patch

providers/dell/idrac.go#L223-L224

Added lines #L223 - L224 were not covered by tests
}

// 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)

Check warning on line 207 in providers/ipmitool/ipmitool.go

View check run for this annotation

Codecov / codecov/patch

providers/ipmitool/ipmitool.go#L206-L207

Added lines #L206 - L207 were not covered by tests
}
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)

Check warning on line 223 in providers/redfish/redfish.go

View check run for this annotation

Codecov / codecov/patch

providers/redfish/redfish.go#L222-L223

Added lines #L222 - L223 were not covered by tests
}
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)

Check warning on line 550 in providers/supermicro/supermicro.go

View check run for this annotation

Codecov / codecov/patch

providers/supermicro/supermicro.go#L549-L550

Added lines #L549 - L550 were not covered by tests
}

0 comments on commit 41bb750

Please sign in to comment.