Skip to content

Commit

Permalink
Merge branch 'jkohnen/probe_hint_named'
Browse files Browse the repository at this point in the history
  • Loading branch information
jwkohnen committed May 29, 2020
2 parents c377a45 + 75fb0e0 commit 80e3a97
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 86 deletions.
108 changes: 88 additions & 20 deletions discover/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,34 @@ package discover
import (
"os"

log "github.com/sirupsen/logrus"

"github.com/bmc-toolbox/bmclib/errors"
"github.com/bmc-toolbox/bmclib/internal/httpclient"
_ "github.com/bmc-toolbox/bmclib/logging" // this make possible to setup logging and properties at any stage
"github.com/bmc-toolbox/bmclib/providers/dummy/ibmc"
)

// this make possible to setup logging and properties at any stage
_ "github.com/bmc-toolbox/bmclib/logging"
log "github.com/sirupsen/logrus"
const (
Probe_hpIlo = "hpilo"
Probe_idrac8 = "idrac8"
Probe_idrac9 = "idrac9"
Probe_supermicrox = "supermicrox"
Probe_hpC7000 = "hpc7000"
Probe_m1000e = "m1000e"
Probe_quanta = "quanta"
Probe_hpCl100 = "hpcl100"
)

// ScanAndConnect will scan the bmc trying to learn the device type and return a working connection
func ScanAndConnect(host string, username string, password string) (bmcConnection interface{}, err error) {
// ScanAndConnect will scan the bmc trying to learn the device type and return a working connection.
func ScanAndConnect(host string, username string, password string, options ...Option) (bmcConnection interface{}, err error) {
log.WithFields(log.Fields{"step": "ScanAndConnect", "host": host}).Debug("detecting vendor")

opts := &Options{HintCallback: func(_ string) error { return nil }}
for _, optFn := range options {
optFn(opts)
}

// return a connection to our dummy device.
if os.Getenv("BMCLIB_TEST") == "1" {
log.WithFields(log.Fields{"step": "ScanAndConnect", "host": host}).Debug("returning connection to dummy ibmc device.")
Expand All @@ -25,22 +40,38 @@ func ScanAndConnect(host string, username string, password string) (bmcConnectio

client, err := httpclient.Build()
if err != nil {
return bmcConnection, err
return nil, err
}

var probe = Probe{client: client, username: username, password: password, host: host}
var devices = []func() (interface{}, error){
probe.hpIlo,
probe.idrac8,
probe.idrac9,
probe.supermicrox,
probe.hpC7000,
probe.m1000e,
probe.quanta,
probe.hpCl100,

var devices = map[string]func() (interface{}, error){
Probe_hpIlo: probe.hpIlo,
Probe_idrac8: probe.idrac8,
Probe_idrac9: probe.idrac9,
Probe_supermicrox: probe.supermicrox,
Probe_hpC7000: probe.hpC7000,
Probe_m1000e: probe.m1000e,
Probe_quanta: probe.quanta,
Probe_hpCl100: probe.hpCl100,
}

for _, probeDevice := range devices {
order := []string{Probe_hpIlo,
Probe_idrac8,
Probe_idrac9,
Probe_supermicrox,
Probe_hpC7000,
Probe_m1000e,
Probe_quanta,
Probe_hpCl100,
}

if opts.Hint != "" {
swapProbe(order, opts.Hint)
}

for _, probeID := range order {
probeDevice := devices[probeID]

log.WithFields(log.Fields{"step": "ScanAndConnect", "host": host}).Debug("probing to identify device")

Expand All @@ -53,13 +84,50 @@ func ScanAndConnect(host string, username string, password string) (bmcConnectio

// at this point it could be a connection error or a errors.ErrUnsupportedHardware
if err != nil {
return bmcConnection, err
return nil, err
}

// return a bmcConnection
return bmcConnection, err
if err := opts.HintCallback(probeID); err != nil {
return nil, err
}

// return a bmcConnection
return bmcConnection, nil
}

return bmcConnection, errors.ErrVendorUnknown
return nil, errors.ErrVendorUnknown
}

type Options struct {
// Hint is a probe ID that hints which probe should be probed first.
Hint string

// HintCallBack is a function that is called back with a probe ID that might be used
// for the next ScanAndConnect attempt. The callback is called only on successful scan.
// If your code persists the hint as "best effort", always return a nil error. Callback is
// synchronous.
HintCallback func(string) error
}

// Option is part of the functional options pattern, see the `With*` functions and
// https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
type Option func(*Options)

// WithProbeHint sets the Options.Hint option.
func WithProbeHint(hint string) Option { return func(args *Options) { args.Hint = hint } }

// WithHintCallBack sets the Options.HintCallback option.
func WithHintCallBack(fn func(string) error) Option {
return func(args *Options) { args.HintCallback = fn }
}

func swapProbe(order []string, hint string) {
// With so few elements and since `==` uses SIMD,
// looping is faster than having yet another hash map.
for i := range order {
if order[i] == hint {
order[0], order[i] = order[i], order[0]
break
}
}
}
155 changes: 89 additions & 66 deletions discover/discovery_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package discover

import (
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"

Expand All @@ -16,104 +16,127 @@ import (
"github.com/spf13/viper"
)

func setup(answers map[string][]byte) (bmc interface{}, err error) {
// setup creates a test server and returns a curried ScanAndConnect() function and a teardown func.
func setup(answers map[string][]byte) (scanAndConnectCurry func(opts ...Option) (bmc interface{}, err error), cancel func()) {
viper.SetDefault("debug", true)
mux = http.NewServeMux()
server = httptest.NewTLSServer(mux)

mux := http.NewServeMux()
server := httptest.NewTLSServer(mux)
ip := strings.TrimPrefix(server.URL, "https://")
username := "super"
password := "test"

for url := range answers {
url := url

mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
w.Write(answers[url])
})
}

bmc, err = ScanAndConnect(ip, username, password)
if err != nil {
return bmc, err
}

return bmc, err
}

func tearDown() {
server.Close()
return func(opts ...Option) (bmc interface{}, err error) {
return ScanAndConnect(ip, username, password, opts...)
},
server.Close
}

func TestProbeHpIlo(t *testing.T) {
bmc, err := setup(answers["Ilo"])
if err != nil {
t.Fatalf("Found errors during the test setup: %v", err)
func TestProbes(t *testing.T) {
testt := []struct {
name string
wantHint string
wantType interface{}
}{
{
name: "SupermicroX",
wantHint: Probe_supermicrox,
wantType: (*supermicrox.SupermicroX)(nil),
},
{
name: "IDrac9",
wantHint: Probe_idrac9,
wantType: (*idrac9.IDrac9)(nil),
},
{
name: "IDrac8",
wantHint: Probe_idrac8,
wantType: (*idrac8.IDrac8)(nil),
},
{
name: "C7000",
wantHint: Probe_hpC7000,
wantType: (*c7000.C7000)(nil),
},
{
name: "Ilo",
wantHint: Probe_hpIlo,
wantType: (*ilo.Ilo)(nil),
},
}
defer tearDown()

if answer, ok := bmc.(*ilo.Ilo); !ok {
fmt.Println(ok)
t.Errorf("Expected answer %T: found %T", &ilo.Ilo{}, answer)
}
for _, tt := range testt {
tt := tt

}
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

func TestProbeHpC7000(t *testing.T) {
bmc, err := setup(answers["C7000"])
if err != nil {
t.Fatalf("Found errors during the test setup: %v", err)
}
scanAndConnect, cancel := setup(_answers[tt.name])
defer cancel()

if answer, ok := bmc.(*c7000.C7000); !ok {
fmt.Println(ok)
t.Errorf("Expected answer %T: found %T", &c7000.C7000{}, answer)
}
hintCallBack := checkHint(t, tt.wantHint)

tearDown()
}
for _, hint := range _hints {
bmc, err := scanAndConnect(WithProbeHint(hint), WithHintCallBack(hintCallBack))
if err != nil {
t.Fatalf("error calling ScanAndConnect(): %v", err)
}

func TestProbeIDrac8(t *testing.T) {
bmc, err := setup(answers["IDrac8"])
if err != nil {
t.Fatalf("Found errors during the test setup %v", err)
}
if reflect.TypeOf(tt.wantType) != reflect.TypeOf(bmc) {
t.Errorf("Want %T, got %T", tt.wantType, bmc)

if answer, ok := bmc.(*idrac8.IDrac8); !ok {
t.Errorf("Expected answer %T: found %T", &idrac8.IDrac8{}, answer)
}
}
}

tearDown()
}
// just for completeness and backward compatibility, do it again w/o optional params
bmc, err := scanAndConnect()
if err != nil {
t.Fatalf("error calling ScanAndConnect(): %v", err)
}

func TestProbeIDrac9(t *testing.T) {
bmc, err := setup(answers["IDrac9"])
if err != nil {
t.Fatalf("Found errors during the test setup %v", err)
}
if reflect.TypeOf(tt.wantType) != reflect.TypeOf(bmc) {
t.Errorf("Want %T, got %T", tt.wantType, bmc)

if answer, ok := bmc.(*idrac9.IDrac9); !ok {
t.Errorf("Expected answer %T: found %T", &idrac9.IDrac9{}, answer)
}
})
}

tearDown()
}

func TestProbeSupermicroX10(t *testing.T) {
bmc, err := setup(answers["SupermicroX"])
if err != nil {
t.Fatalf("Found errors during the test setup %v", err)
}
func checkHint(t *testing.T, want string) func(string) error {
return func(got string) error {
t.Helper()

if answer, ok := bmc.(*supermicrox.SupermicroX); !ok {
t.Errorf("Expected answer %T: found %T", &supermicrox.SupermicroX{}, answer)
}
if got != want {
t.Errorf("hint call back returned wrong hint: want: %q, got: %q", want, got)
}

tearDown()
return nil
}
}

var (
mux *http.ServeMux
server *httptest.Server
answers = map[string]map[string][]byte{
_hints = []string{
"",
"garbage",
Probe_hpIlo,
Probe_idrac8,
Probe_idrac9,
Probe_supermicrox,
Probe_hpC7000,
Probe_m1000e,
Probe_quanta,
Probe_hpCl100,
}

_answers = map[string]map[string][]byte{
"IDrac8": {"/session": []byte(`{"aimGetProp" : {"hostname" :"machine","gui_str_title_bar" :"","OEMHostName" :"machine.example.com","fwVersion" :"2.50.33","sysDesc" :"PowerEdge M630","status" : "OK"}}`)},
"IDrac9": {"/sysmgmt/2015/bmc/info": []byte(`{"Attributes":{"ADEnabled":"Disabled","BuildVersion":"37","FwVer":"3.15.15.15","GUITitleBar":"spare-H16Z4M2","IsOEMBranded":"0","License":"Enterprise","SSOEnabled":"Disabled","SecurityPolicyMessage":"By accessing this computer, you confirm that such access complies with your organization's security policy.","ServerGen":"14G","SrvPrcName":"NULL","SystemLockdown":"Disabled","SystemModelName":"PowerEdge M640","TFAEnabled":"Disabled","iDRACName":"spare-H16Z4M2"}}`)},
"SupermicroX": {"/cgi/login.cgi": []byte("ATEN International")},
Expand Down

0 comments on commit 80e3a97

Please sign in to comment.