From 1f2f0d84745169b312f97b2f6b4db5eb58fd02df Mon Sep 17 00:00:00 2001 From: Zev Weiss Date: Thu, 16 Nov 2023 22:36:47 -0800 Subject: [PATCH 1/2] Add DeactivateSOL method This method will terminate an SOL (serial-over-lan) session currently active on the BMC (if there is one). The only provider implementing it is ipmitool, via 'ipmitool sol deactivate'. --- bmc/sol.go | 70 ++++++++++++++++++++++++++++++++++ client.go | 9 +++++ internal/ipmi/ipmi.go | 10 +++++ providers/ipmitool/ipmitool.go | 6 +++ providers/providers.go | 3 ++ 5 files changed, 98 insertions(+) create mode 100644 bmc/sol.go diff --git a/bmc/sol.go b/bmc/sol.go new file mode 100644 index 00000000..89c172b0 --- /dev/null +++ b/bmc/sol.go @@ -0,0 +1,70 @@ +package bmc + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" +) + +// SOLDeactivator for deactivating SOL sessions on a BMC. +type SOLDeactivator interface { + DeactivateSOL(ctx context.Context) (err error) +} + +// deactivatorProvider is an internal struct to correlate an implementation/provider and its name +type deactivatorProvider struct { + name string + solDeactivator SOLDeactivator +} + +// deactivateSOL tries all implementations for a successful SOL deactivation +func deactivateSOL(ctx context.Context, timeout time.Duration, b []deactivatorProvider) (metadata Metadata, err error) { + var metadataLocal Metadata + + for _, elem := range b { + if elem.solDeactivator == 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() + newErr := elem.solDeactivator.DeactivateSOL(ctx) + if newErr != nil { + err = multierror.Append(err, errors.WithMessagef(newErr, "provider: %v", elem.name)) + continue + } + metadataLocal.SuccessfulProvider = elem.name + return metadataLocal, nil + } + } + return metadataLocal, multierror.Append(err, errors.New("failed to deactivate SOL session")) +} + +// DeactivateSOLFromInterfaces identifies implementations of the SOLDeactivator interface and passes them to the deactivateSOL() wrapper method. +func DeactivateSOLFromInterfaces(ctx context.Context, timeout time.Duration, generic []interface{}) (metadata Metadata, err error) { + deactivators := make([]deactivatorProvider, 0) + for _, elem := range generic { + temp := deactivatorProvider{name: getProviderName(elem)} + switch p := elem.(type) { + case SOLDeactivator: + temp.solDeactivator = p + deactivators = append(deactivators, temp) + default: + e := fmt.Sprintf("not an SOLDeactivator implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(deactivators) == 0 { + return metadata, multierror.Append(err, errors.New("no SOLDeactivator implementations found")) + } + return deactivateSOL(ctx, timeout, deactivators) +} diff --git a/client.go b/client.go index 106e58d1..f93d7602 100644 --- a/client.go +++ b/client.go @@ -526,6 +526,15 @@ func (c *Client) ResetBMC(ctx context.Context, resetType string) (ok bool, err e return ok, err } +// DeactivateSOL pass through library function to deactivate active SOL sessions +func (c *Client) DeactivateSOL(ctx context.Context) (err error) { + ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "DeactivateSOL") + defer span.End() + metadata, err := bmc.DeactivateSOLFromInterfaces(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) + c.setMetadata(metadata) + return err +} + // Inventory pass through library function to collect hardware and firmware inventory func (c *Client) Inventory(ctx context.Context) (device *common.Device, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "Inventory") diff --git a/internal/ipmi/ipmi.go b/internal/ipmi/ipmi.go index 427a76ae..c97c7a4e 100644 --- a/internal/ipmi/ipmi.go +++ b/internal/ipmi/ipmi.go @@ -427,3 +427,13 @@ func (i *Ipmi) GetSystemEventLogRaw(ctx context.Context) (eventlog string, err e return output, nil } + +func (i *Ipmi) DeactivateSOL(ctx context.Context) (err error) { + out, err := i.run(ctx, []string{"sol", "deactivate"}) + // Don't treat this as a failure (we just want to ensure there + // isn't an active SOL session left open) + if strings.TrimSpace(out) == "Info: SOL payload already de-activated" { + err = nil + } + return err +} diff --git a/providers/ipmitool/ipmitool.go b/providers/ipmitool/ipmitool.go index d8b5d9b2..d285161f 100644 --- a/providers/ipmitool/ipmitool.go +++ b/providers/ipmitool/ipmitool.go @@ -30,6 +30,7 @@ var ( providers.FeatureClearSystemEventLog, providers.FeatureGetSystemEventLog, providers.FeatureGetSystemEventLogRaw, + providers.FeatureDeactivateSOL, } ) @@ -149,6 +150,11 @@ func (c *Conn) BmcReset(ctx context.Context, resetType string) (ok bool, err err return c.ipmitool.PowerResetBmc(ctx, resetType) } +// DeactivateSOL will deactivate active SOL sessions +func (c *Conn) DeactivateSOL(ctx context.Context) (err error) { + return c.ipmitool.DeactivateSOL(ctx) +} + // UserRead list all users func (c *Conn) UserRead(ctx context.Context) (users []map[string]string, err error) { return c.ipmitool.ReadUsers(ctx) diff --git a/providers/providers.go b/providers/providers.go index dc300577..96eb97b6 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -60,4 +60,7 @@ const ( // FeatureFirmwareUploadInitiateInstall identifies an implementation that uploads firmware _and_ initiates the install process. FeatureFirmwareUploadInitiateInstall registrar.Feature = "uploadandinitiateinstall" + + // FeatureDeactivateSOL means an implementation that can deactivate active SOL sessions + FeatureDeactivateSOL registrar.Feature = "deactivatesol" ) From 288790dba5b8c534126a8f109b3f35c0a93b3045 Mon Sep 17 00:00:00 2001 From: Zev Weiss Date: Wed, 20 Dec 2023 00:50:25 -0800 Subject: [PATCH 2/2] Add tests for SOL deactivation --- bmc/sol_test.go | 99 +++++++++++++++++++++++++++++ providers/ipmitool/ipmitool_test.go | 18 ++++++ 2 files changed, 117 insertions(+) create mode 100644 bmc/sol_test.go diff --git a/bmc/sol_test.go b/bmc/sol_test.go new file mode 100644 index 00000000..1623a027 --- /dev/null +++ b/bmc/sol_test.go @@ -0,0 +1,99 @@ +package bmc + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-multierror" +) + +type solTermTester struct { + MakeErrorOut bool +} + +func (r *solTermTester) DeactivateSOL(ctx context.Context) (err error) { + if r.MakeErrorOut { + return errors.New("SOL deactivation failed") + } + return nil +} + +func (r *solTermTester) Name() string { + return "test provider" +} + +func TestDeactivateSOL(t *testing.T) { + testCases := map[string]struct { + makeErrorOut bool + err error + ctxTimeout time.Duration + }{ + "success": {makeErrorOut: false}, + "error": {makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider: SOL deactivation failed"), errors.New("failed to deactivate SOL session")}}}, + "error context timeout": {makeErrorOut: false, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + testImplementation := solTermTester{MakeErrorOut: tc.makeErrorOut} + if tc.ctxTimeout == 0 { + tc.ctxTimeout = time.Second * 3 + } + ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) + defer cancel() + _, err := deactivateSOL(ctx, 0, []deactivatorProvider{{"test provider", &testImplementation}}) + var diff string + if err != nil && tc.err != nil { + diff = cmp.Diff(err.Error(), tc.err.Error()) + } else { + diff = cmp.Diff(err, tc.err) + } + if diff != "" { + t.Fatal(diff) + } + }) + } +} + +func TestDeactivateSOLFromInterfaces(t *testing.T) { + testCases := map[string]struct { + err error + badImplementation bool + withName bool + }{ + "success": {}, + "success with metadata": {withName: true}, + "no implementations found": {badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not an SOLDeactivator implementation: *struct {}"), errors.New("no SOLDeactivator implementations found")}}}, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + var generic []interface{} + if tc.badImplementation { + badImplementation := struct{}{} + generic = []interface{}{&badImplementation} + } else { + testImplementation := solTermTester{} + generic = []interface{}{&testImplementation} + } + metadata, err := DeactivateSOLFromInterfaces(context.Background(), 0, generic) + var diff string + if err != nil && tc.err != nil { + diff = cmp.Diff(err.Error(), tc.err.Error()) + } else { + diff = cmp.Diff(err, tc.err) + } + if diff != "" { + t.Fatal(diff) + } + if tc.withName { + if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { + t.Fatal(diff) + } + } + }) + } +} diff --git a/providers/ipmitool/ipmitool_test.go b/providers/ipmitool/ipmitool_test.go index 241503f1..90f563e8 100644 --- a/providers/ipmitool/ipmitool_test.go +++ b/providers/ipmitool/ipmitool_test.go @@ -107,6 +107,24 @@ func TestBMCReset(t *testing.T) { t.Fatal() } +func TestDeactivateSOL(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.DeactivateSOL(context.Background()) + if err != nil { + t.Fatal(err) + } + t.Log(err != nil) + t.Fatal() +} + func TestSystemEventLogClear(t *testing.T) { t.Skip("need real ipmi server") host := "127.0.0.1"