Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for sending NMI #384

Merged
merged 1 commit into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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
}
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 @@

system.DisableEtagMatch(c.disableEtagMatch)


err = system.Reset(rf.ForceOffResetType)
if err != nil {
return false, err
Expand All @@ -221,3 +220,23 @@

return true, nil
}

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

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

View check run for this annotation

Codecov / codecov/patch

internal/redfishwrapper/power.go#L225-L227

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

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

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

View check run for this annotation

Codecov / codecov/patch

internal/redfishwrapper/power.go#L230-L232

Added lines #L230 - L232 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

internal/redfishwrapper/power.go#L235-L237

Added lines #L235 - L237 were not covered by tests
}
}

return nil

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

View check run for this annotation

Codecov / codecov/patch

internal/redfishwrapper/power.go#L241

Added line #L241 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 @@
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) 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) 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 @@

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