diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..893a422f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go +{ + "name": "Go", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/go:1-1.21-bullseye" + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/bmc/sel.go b/bmc/sel.go new file mode 100644 index 00000000..25c6cfc7 --- /dev/null +++ b/bmc/sel.go @@ -0,0 +1,69 @@ +package bmc + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" +) + +// System Event Log Services for related services +type SystemEventLog interface { + ClearSystemEventLog(ctx context.Context) (err error) +} + +type systemEventLogProviders struct { + name string + systemEventLogProvider SystemEventLog +} + +func clearSystemEventLog(ctx context.Context, timeout time.Duration, s []systemEventLogProviders) (metadata Metadata, err error) { + var metadataLocal Metadata + + for _, elem := range s { + if elem.systemEventLogProvider == nil { + continue + } + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + + return metadata, err + default: + metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + selErr := elem.systemEventLogProvider.ClearSystemEventLog(ctx) + if selErr != nil { + err = multierror.Append(err, errors.WithMessagef(selErr, "provider: %v", elem.name)) + continue + } + metadataLocal.SuccessfulProvider = elem.name + return metadataLocal, nil + } + + } + + return metadataLocal, multierror.Append(err, errors.New("failed to reset System Event Log")) +} + +func ClearSystemEventLogFromInterfaces(ctx context.Context, timeout time.Duration, generic []interface{}) (metadata Metadata, err error) { + selServices := make([]systemEventLogProviders, 0) + for _, elem := range generic { + temp := systemEventLogProviders{name: getProviderName(elem)} + switch p := elem.(type) { + case SystemEventLog: + temp.systemEventLogProvider = p + selServices = append(selServices, temp) + default: + e := fmt.Sprintf("not a SystemEventLog service implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(selServices) == 0 { + return metadata, multierror.Append(err, errors.New("no SystemEventLog implementations found")) + } + return clearSystemEventLog(ctx, timeout, selServices) +} diff --git a/bmc/sel_test.go b/bmc/sel_test.go new file mode 100644 index 00000000..88b10d13 --- /dev/null +++ b/bmc/sel_test.go @@ -0,0 +1,61 @@ +package bmc + +import ( + "context" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +type mockSystemEventLogService struct { + name string + err error +} + +func (m *mockSystemEventLogService) ClearSystemEventLog(ctx context.Context) error { + return m.err +} + +func (m *mockSystemEventLogService) Name() string { + return m.name +} + +func TestClearSystemEventLog(t *testing.T) { + ctx := context.Background() + timeout := 1 * time.Second + + // Test with a mock SystemEventLogService that returns nil + mockService := &mockSystemEventLogService{name: "mock1", err: nil} + metadata, err := clearSystemEventLog(ctx, timeout, []systemEventLogProviders{{name: mockService.name, systemEventLogProvider: mockService}}) + assert.Nil(t, err) + assert.Equal(t, mockService.name, metadata.SuccessfulProvider) + + // Test with a mock SystemEventLogService that returns an error + mockService = &mockSystemEventLogService{name: "mock2", err: errors.New("mock error")} + metadata, err = clearSystemEventLog(ctx, timeout, []systemEventLogProviders{{name: mockService.name, systemEventLogProvider: mockService}}) + assert.NotNil(t, err) + assert.NotEqual(t, mockService.name, metadata.SuccessfulProvider) +} + +func TestClearSystemEventLogFromInterfaces(t *testing.T) { + ctx := context.Background() + timeout := 1 * time.Second + + // Test with an empty slice + metadata, err := ClearSystemEventLogFromInterfaces(ctx, timeout, []interface{}{}) + assert.NotNil(t, err) + assert.Empty(t, metadata.SuccessfulProvider) + + // Test with a slice containing a non-SystemEventLog object + metadata, err = ClearSystemEventLogFromInterfaces(ctx, timeout, []interface{}{"not a SystemEventLog Service"}) + assert.NotNil(t, err) + assert.Empty(t, metadata.SuccessfulProvider) + + // Test with a slice containing a mock SystemEventLogService that returns nil + mockService := &mockSystemEventLogService{name: "mock1"} + metadata, err = ClearSystemEventLogFromInterfaces(ctx, timeout, []interface{}{mockService}) + assert.Nil(t, err) + assert.Equal(t, mockService.name, metadata.SuccessfulProvider) +} diff --git a/client.go b/client.go index b552cce5..437a919d 100644 --- a/client.go +++ b/client.go @@ -405,3 +405,9 @@ func (c *Client) Screenshot(ctx context.Context) (image []byte, fileType string, return image, fileType, err } + +func (c *Client) ClearSystemEventLog(ctx context.Context) (err error) { + metadata, err := bmc.ClearSystemEventLogFromInterfaces(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) + c.setMetadata(metadata) + return err +} diff --git a/examples/sel/main.go b/examples/sel/main.go new file mode 100644 index 00000000..114d6496 --- /dev/null +++ b/examples/sel/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "crypto/x509" + "flag" + "io/ioutil" + "time" + + "github.com/bmc-toolbox/bmclib/v2" + "github.com/bmc-toolbox/bmclib/v2/providers" + "github.com/bombsimon/logrusr/v2" + "github.com/sirupsen/logrus" +) + +func main() { + user := flag.String("user", "", "Username to login with") + pass := flag.String("password", "", "Username to login with") + host := flag.String("host", "", "BMC hostname to connect to") + withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") + certPoolFile := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") + flag.Parse() + + l := logrus.New() + l.Level = logrus.DebugLevel + logger := logrusr.New(l) + + if *host == "" || *user == "" || *pass == "" { + l.Fatal("required host/user/pass parameters not defined") + } + + clientOpts := []bmclib.Option{ + bmclib.WithLogger(logger), + bmclib.WithRedfishUseBasicAuth(true), + } + + if *withSecureTLS { + var pool *x509.CertPool + if *certPoolFile != "" { + pool = x509.NewCertPool() + data, err := ioutil.ReadFile(*certPoolFile) + if err != nil { + l.Fatal(err) + } + pool.AppendCertsFromPEM(data) + } + // a nil pool uses the system certs + clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) + } + + cl := bmclib.NewClient(*host, *user, *pass, clientOpts...) + cl.Registry.Drivers = cl.Registry.Supports(providers.FeatureClearSystemEventLog) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + err := cl.Open(ctx) + if err != nil { + l.WithError(err).Fatal(err, "BMC login failed") + } + defer cl.Close(ctx) + + err = cl.ClearSystemEventLog(ctx) + if err != nil { + l.WithError(err).Fatal(err, "failed to clear System Event Log") + } + l.Info("System Event Log cleared") +} diff --git a/internal/ipmi/ipmi.go b/internal/ipmi/ipmi.go index 8b036794..5161b7dd 100644 --- a/internal/ipmi/ipmi.go +++ b/internal/ipmi/ipmi.go @@ -377,3 +377,9 @@ func (i *Ipmi) ReadUsers(ctx context.Context) (users []map[string]string, err er return users, err } + +// ClearSystemEventLog clears the system event log +func (i *Ipmi) ClearSystemEventLog(ctx context.Context) (err error) { + _, err = i.run(ctx, []string{"sel", "clear"}) + return err +} diff --git a/internal/redfishwrapper/sel.go b/internal/redfishwrapper/sel.go new file mode 100644 index 00000000..78798735 --- /dev/null +++ b/internal/redfishwrapper/sel.go @@ -0,0 +1,36 @@ +package redfishwrapper + +import ( + "context" + + bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" + "github.com/pkg/errors" +) + +// ClearSystemEventLog clears all of the LogServices logs +func (c *Client) ClearSystemEventLog(ctx context.Context) (err error) { + if err := c.SessionActive(); err != nil { + return errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) + } + + chassis, err := c.client.Service.Chassis() + if err != nil { + return err + } + + for _, c := range chassis { + logServices, err := c.LogServices() + if err != nil { + return err + } + + for _, logService := range logServices { + err = logService.ClearLog() + if err != nil { + return err + } + } + } + + return nil +} diff --git a/providers/ipmitool/ipmitool.go b/providers/ipmitool/ipmitool.go index e104884e..90aedbd4 100644 --- a/providers/ipmitool/ipmitool.go +++ b/providers/ipmitool/ipmitool.go @@ -27,6 +27,7 @@ var ( providers.FeatureUserRead, providers.FeatureBmcReset, providers.FeatureBootDeviceSet, + providers.FeatureClearSystemEventLog, } ) @@ -180,3 +181,7 @@ func (c *Conn) PowerSet(ctx context.Context, state string) (ok bool, err error) return ok, err } + +func (c *Conn) ClearSystemEventLog(ctx context.Context) (err error) { + return c.ipmitool.ClearSystemEventLog(ctx) +} diff --git a/providers/ipmitool/ipmitool_test.go b/providers/ipmitool/ipmitool_test.go index ffe8b7bb..0e9589c2 100644 --- a/providers/ipmitool/ipmitool_test.go +++ b/providers/ipmitool/ipmitool_test.go @@ -106,3 +106,21 @@ func TestBMCReset(t *testing.T) { t.Log(state) t.Fatal() } + +func TestSystemEventLogClear(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.ClearSystemEventLog(context.Background()) + if err != nil { + t.Fatal(err) + } + t.Log("System Event Log cleared") + t.Fatal() +} diff --git a/providers/providers.go b/providers/providers.go index 64c587a4..28ffd83a 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -33,4 +33,6 @@ const ( FeaturePostCodeRead registrar.Feature = "postcoderead" // FeatureScreenshot means an implementation that returns a screenshot of the video. FeatureScreenshot registrar.Feature = "screenshot" + // FeatureClearSystemEventLog means an implementation that clears the BMC System Event Log (SEL) + FeatureClearSystemEventLog registrar.Feature = "clearsystemeventlog" ) diff --git a/providers/redfish/redfish.go b/providers/redfish/redfish.go index 96a8861f..22fc4e19 100644 --- a/providers/redfish/redfish.go +++ b/providers/redfish/redfish.go @@ -37,6 +37,7 @@ var ( providers.FeatureFirmwareInstall, providers.FeatureFirmwareInstallStatus, providers.FeatureBmcReset, + providers.FeatureClearSystemEventLog, } ) diff --git a/providers/redfish/sel.go b/providers/redfish/sel.go new file mode 100644 index 00000000..9d516243 --- /dev/null +++ b/providers/redfish/sel.go @@ -0,0 +1,7 @@ +package redfish + +import "context" + +func (c *Conn) ClearSystemEventLog(ctx context.Context) (err error) { + return c.redfishwrapper.ClearSystemEventLog(ctx) +}