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

Implements screenshot feature for dell #333

Merged
merged 4 commits into from
May 15, 2023
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
76 changes: 76 additions & 0 deletions bmc/screenshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package bmc

import (
"context"
"fmt"

bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
)

// ScreenshotGetter interface provides methods to query for a BMC screen capture.
type ScreenshotGetter interface {
Screenshot(ctx context.Context) (image []byte, fileType string, err error)
}

type screenshotGetterProvider struct {
name string
ScreenshotGetter
}

// screenshot returns an image capture of the video output.
func screenshot(ctx context.Context, generic []screenshotGetterProvider) (image []byte, fileType string, metadata Metadata, err error) {
var metadataLocal Metadata

for _, elem := range generic {
if elem.ScreenshotGetter == nil {
continue
}
select {
case <-ctx.Done():
err = multierror.Append(err, ctx.Err())

return image, fileType, metadata, err
default:
metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name)
image, fileType, vErr := elem.Screenshot(ctx)
if vErr != nil {
err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name))
continue

}
metadataLocal.SuccessfulProvider = elem.name
return image, fileType, metadataLocal, nil
}
}

return image, fileType, metadataLocal, multierror.Append(err, errors.New("failed to capture screenshot"))
}

// ScreenshotFromInterfaces identifies implementations of the ScreenshotGetter interface and passes the found implementations to the screenshot() wrapper method.
func ScreenshotFromInterfaces(ctx context.Context, generic []interface{}) (image []byte, fileType string, metadata Metadata, err error) {
chrisdoherty4 marked this conversation as resolved.
Show resolved Hide resolved
implementations := make([]screenshotGetterProvider, 0)
for _, elem := range generic {
temp := screenshotGetterProvider{name: getProviderName(elem)}
switch p := elem.(type) {
case ScreenshotGetter:
temp.ScreenshotGetter = p
implementations = append(implementations, temp)
default:
e := fmt.Sprintf("not a ScreenshotGetter implementation: %T", p)
err = multierror.Append(err, errors.New(e))
}
}
if len(implementations) == 0 {
return image, fileType, metadata, multierror.Append(
err,
errors.Wrap(
bmclibErrs.ErrProviderImplementation,
("no ScreenshotGetter implementations found"),
),
)
}

return screenshot(ctx, implementations)
}
110 changes: 110 additions & 0 deletions bmc/screenshot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package bmc

import (
"context"
"errors"
"testing"
"time"

"github.com/hashicorp/go-multierror"
"gopkg.in/go-playground/assert.v1"
)

type screenshotTester struct {
MakeErrorOut bool
}

func (r *screenshotTester) Screenshot(ctx context.Context) (img []byte, fileType string, err error) {
if r.MakeErrorOut {
return nil, "", errors.New("crappy bmc is crappy")
}

return []byte(`foobar`), "png", nil
}

func (r *screenshotTester) Name() string {
return "test screenshot provider"
}

func TestScreenshot(t *testing.T) {
testCases := map[string]struct {
makeErrorOut bool
wantImage []byte
wantFileType string
wantSuccessfulProvider string
wantProvidersAttempted []string
wantErr error
ctxTimeout time.Duration
}{
"success": {false, []byte(`foobar`), "png", "test provider", []string{"test provider"}, nil, 1 * time.Second},
"error": {true, nil, "", "", []string{"test provider"}, &multierror.Error{Errors: []error{errors.New("provider: test provider: crappy bmc is crappy"), errors.New("failed to capture screenshot")}}, 1 * time.Second},
"error context timeout": {true, nil, "", "", nil, &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, 1 * time.Nanosecond},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
testImplementation := screenshotTester{MakeErrorOut: tc.makeErrorOut}
if tc.ctxTimeout == 0 {
tc.ctxTimeout = time.Second * 3
}

ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout)
defer cancel()
image, fileType, metadata, err := screenshot(ctx, []screenshotGetterProvider{{"test provider", &testImplementation}})
if err != nil {
if tc.wantErr == nil {
t.Fatal(err)
}

assert.Equal(t, tc.wantErr.Error(), err.Error())
} else {
assert.Equal(t, tc.wantImage, image)
assert.Equal(t, tc.wantFileType, fileType)
}

assert.Equal(t, tc.wantProvidersAttempted, metadata.ProvidersAttempted)
assert.Equal(t, tc.wantSuccessfulProvider, metadata.SuccessfulProvider)
})
}
}

func TestScreenshotFromInterfaces(t *testing.T) {
testCases := map[string]struct {
wantImage []byte
wantFileType string
wantSuccessfulProvider string
wantProvidersAttempted []string
wantErr error
badImplementation bool
}{
"success with metadata": {[]byte(`foobar`), "png", "test screenshot provider", []string{"test screenshot provider"}, nil, false},
"no implementations found": {nil, "", "", nil, &multierror.Error{Errors: []error{errors.New("not a ScreenshotGetter implementation: *struct {}"), errors.New("no ScreenshotGetter implementations found: error in provider implementation")}}, true},
}

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 := screenshotTester{}
generic = []interface{}{&testImplementation}
}
image, fileType, metadata, err := ScreenshotFromInterfaces(context.Background(), generic)
if err != nil {
if tc.wantErr == nil {
t.Fatal(err)
}

assert.Equal(t, tc.wantErr.Error(), err.Error())
} else {
assert.Equal(t, tc.wantImage, image)
assert.Equal(t, tc.wantFileType, fileType)
}

assert.Equal(t, tc.wantProvidersAttempted, metadata.ProvidersAttempted)
assert.Equal(t, tc.wantSuccessfulProvider, metadata.SuccessfulProvider)
})
}
}
25 changes: 25 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/bmc-toolbox/bmclib/v2/bmc"
"github.com/bmc-toolbox/bmclib/v2/internal/httpclient"
"github.com/bmc-toolbox/bmclib/v2/providers/asrockrack"
"github.com/bmc-toolbox/bmclib/v2/providers/dell"
"github.com/bmc-toolbox/bmclib/v2/providers/intelamt"
"github.com/bmc-toolbox/bmclib/v2/providers/ipmitool"
"github.com/bmc-toolbox/bmclib/v2/providers/redfish"
Expand Down Expand Up @@ -54,6 +55,7 @@ type providerConfig struct {
asrock asrockrack.Config
gofish redfish.Config
intelamt intelamt.Config
dell dell.Config
}

// NewClient returns a new Client struct
Expand All @@ -79,6 +81,10 @@ func NewClient(host, user, pass string, opts ...Option) *Client {
HostScheme: "http",
Port: 16992,
},
dell: dell.Config{
Port: "443",
VersionsNotCompatible: []string{},
},
},
}

Expand Down Expand Up @@ -158,6 +164,18 @@ func (c *Client) registerProviders() {
}
driverAMT := intelamt.New(c.Auth.Host, c.Auth.User, c.Auth.Pass, iamtOpts...)
c.Registry.Register(intelamt.ProviderName, intelamt.ProviderProtocol, intelamt.Features, nil, driverAMT)

// register Dell gofish provider
dellGofishHttpClient := *c.httpClient
//dellGofishHttpClient.Transport = c.httpClient.Transport.(*http.Transport).Clone()
dellGofishOpts := []dell.Option{
dell.WithHttpClient(&dellGofishHttpClient),
dell.WithVersionsNotCompatible(c.providerConfig.dell.VersionsNotCompatible),
dell.WithUseBasicAuth(c.providerConfig.dell.UseBasicAuth),
dell.WithPort(c.providerConfig.dell.Port),
}
driverGoFishDell := dell.New(c.Auth.Host, c.Auth.User, c.Auth.Pass, c.Logger, dellGofishOpts...)
c.Registry.Register(dell.ProviderName, redfish.ProviderProtocol, dell.Features, nil, driverGoFishDell)
}

// GetMetadata returns the metadata that is populated after each BMC function/method call
Expand Down Expand Up @@ -342,3 +360,10 @@ func (c *Client) PostCode(ctx context.Context) (status string, code int, err err
c.setMetadata(metadata)
return status, code, err
}

func (c *Client) Screenshot(ctx context.Context) (image []byte, fileType string, err error) {
image, fileType, metadata, err := bmc.ScreenshotFromInterfaces(ctx, c.registry().GetDriverInterfaces())
c.setMetadata(metadata)

return image, fileType, err
}
3 changes: 3 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ var (

// ErrNoBiosAttributes is returned when no bios attributes are available from the BMC.
ErrNoBiosAttributes = errors.New("no BIOS attributes available")

// ErrScreenshot is returned when screen capture fails.
ErrScreenshot = errors.New("error in capturing screen")
)

type ErrUnsupportedHardware struct {
Expand Down
20 changes: 20 additions & 0 deletions examples/screenshot/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
status is an example commmand that utilizes the 'v1' bmclib interface methods
to capture a screenshot.

$ go run ./examples/v1/status/main.go -h
Usage of /tmp/go-build1941100323/b001/exe/main:
-cert-pool string
Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true
-host string
BMC hostname to connect to
-password string
Username to login with
-port int
BMC port to connect to (default 443)
-secure-tls
Enable secure TLS
-user string
Username to login with
*/
package main
93 changes: 93 additions & 0 deletions examples/screenshot/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package main

import (
"context"
"crypto/x509"
"flag"
"fmt"
"io/ioutil"
"os"
"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")
port := flag.String("port", "443", "BMC port 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.WithRedfishPort(*port),
}

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.FeatureScreenshot)

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

return
}
defer cl.Close(ctx)

image, fileType, err := cl.Screenshot(ctx)
if err != nil {
l.WithError(err).Error()

return
}

filename := fmt.Sprintf("screenshot." + fileType)
fh, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
l.WithError(err).Error()

return
}

defer fh.Close()

_, err = fh.Write(image)
if err != nil {
l.WithError(err).Error()

return
}

l.Info("screenshot saved as: " + filename)
}
9 changes: 9 additions & 0 deletions internal/redfishwrapper/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func (c *Client) Open(ctx context.Context) error {
if c.port != "" {
endpoint = c.host + ":" + c.port
}

config := gofish.ClientConfig{
Endpoint: endpoint,
Username: c.user,
Expand Down Expand Up @@ -189,3 +190,11 @@ func (c *Client) VersionCompatible() bool {

return !slices.Contains(c.versionsNotCompatible, c.client.Service.RedfishVersion)
}

func (c *Client) PostWithHeaders(ctx context.Context, url string, payload interface{}, headers map[string]string) (*http.Response, error) {
return c.client.PostWithHeaders(url, payload, headers)
}

func (c *Client) PatchWithHeaders(ctx context.Context, url string, payload interface{}, headers map[string]string) (*http.Response, error) {
return c.client.PatchWithHeaders(url, payload, headers)
}
Loading