diff --git a/bmc/firmware.go b/bmc/firmware.go new file mode 100644 index 00000000..ba906208 --- /dev/null +++ b/bmc/firmware.go @@ -0,0 +1,200 @@ +package bmc + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/hashicorp/go-multierror" +) + +// BMCVersionGetter retrieves the current BMC firmware version information +type BMCVersionGetter interface { + GetBMCVersion(ctx context.Context) (version string, err error) +} + +// BMCFirmwareUpdater upgrades the BMC firmware +type BMCFirmwareUpdater interface { + FirmwareUpdateBMC(ctx context.Context, fileReader io.Reader) (err error) +} + +// BIOSVersionGetter retrieves the current BIOS firmware version information +type BIOSVersionGetter interface { + GetBIOSVersion(ctx context.Context) (version string, err error) +} + +// BIOSFirmwareUpdater upgrades the BIOS firmware +type BIOSFirmwareUpdater interface { + FirmwareUpdateBIOS(ctx context.Context, fileReader io.Reader) (err error) +} + +// GetBMCVersion returns the BMC firmware version, trying all interface implementations passed in +func GetBMCVersion(ctx context.Context, p []BMCVersionGetter) (version string, err error) { +Loop: + for _, elem := range p { + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + break Loop + default: + if elem != nil { + version, vErr := elem.GetBMCVersion(ctx) + if vErr != nil { + err = multierror.Append(err, vErr) + continue + } + return version, nil + } + } + } + + return version, multierror.Append(err, errors.New("failed to get BMC version")) +} + +// GetBMCVersionFromInterfaces pass through to library function +func GetBMCVersionFromInterfaces(ctx context.Context, generic []interface{}) (version string, err error) { + bmcVersionGetter := make([]BMCVersionGetter, 0) + for _, elem := range generic { + switch p := elem.(type) { + case BMCVersionGetter: + bmcVersionGetter = append(bmcVersionGetter, p) + default: + e := fmt.Sprintf("not a BMCVersionGetter implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(bmcVersionGetter) == 0 { + return version, multierror.Append(err, errors.New("no BMCVersionGetter implementations found")) + } + + return GetBMCVersion(ctx, bmcVersionGetter) +} + +// UpdateBMCFirmware upgrades the BMC firmware, trying all interface implementations passed ini +func UpdateBMCFirmware(ctx context.Context, fileReader io.Reader, p []BMCFirmwareUpdater) (err error) { +Loop: + for _, elem := range p { + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + break Loop + default: + if elem != nil { + uErr := elem.FirmwareUpdateBMC(ctx, fileReader) + if uErr != nil { + err = multierror.Append(err, uErr) + continue + } + return nil + } + } + } + + return multierror.Append(err, errors.New("failed to update BMC firmware")) + +} + +// UpdateBMCFirmwareFromInterfaces pass through to library function +func UpdateBMCFirmwareFromInterfaces(ctx context.Context, fileReader io.Reader, generic []interface{}) (err error) { + bmcFirmwareUpdater := make([]BMCFirmwareUpdater, 0) + for _, elem := range generic { + switch p := elem.(type) { + case BMCFirmwareUpdater: + bmcFirmwareUpdater = append(bmcFirmwareUpdater, p) + default: + e := fmt.Sprintf("not a BMCFirmwareUpdater implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(bmcFirmwareUpdater) == 0 { + return multierror.Append(err, errors.New("no BMCFirmwareUpdater implementations found")) + } + + return UpdateBMCFirmware(ctx, fileReader, bmcFirmwareUpdater) +} + +// GetBIOSVersion returns the BMC firmware version, trying all interface implementations passed in +func GetBIOSVersion(ctx context.Context, p []BIOSVersionGetter) (version string, err error) { +Loop: + for _, elem := range p { + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + break Loop + default: + if elem != nil { + version, vErr := elem.GetBIOSVersion(ctx) + if vErr != nil { + err = multierror.Append(err, vErr) + continue + } + return version, nil + } + } + } + + return version, multierror.Append(err, errors.New("failed to get BIOS version")) +} + +// GetBIOSVersionFromInterfaces pass through to library function +func GetBIOSVersionFromInterfaces(ctx context.Context, generic []interface{}) (version string, err error) { + biosVersionGetter := make([]BIOSVersionGetter, 0) + for _, elem := range generic { + switch p := elem.(type) { + case BIOSVersionGetter: + biosVersionGetter = append(biosVersionGetter, p) + default: + e := fmt.Sprintf("not a BIOSVersionGetter implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(biosVersionGetter) == 0 { + return version, multierror.Append(err, errors.New("no BIOSVersionGetter implementations found")) + } + + return GetBIOSVersion(ctx, biosVersionGetter) +} + +// UpdateBIOSFirmware upgrades the BIOS firmware, trying all interface implementations passed ini +func UpdateBIOSFirmware(ctx context.Context, fileReader io.Reader, p []BIOSFirmwareUpdater) (err error) { +Loop: + for _, elem := range p { + select { + case <-ctx.Done(): + err = multierror.Append(err, ctx.Err()) + break Loop + default: + if elem != nil { + uErr := elem.FirmwareUpdateBIOS(ctx, fileReader) + if uErr != nil { + err = multierror.Append(err, uErr) + continue + } + return nil + } + } + } + + return multierror.Append(err, errors.New("failed to update BIOS firmware")) + +} + +// GetBMCVersionFromInterfaces pass through to library function +func UpdateBIOSFirmwareFromInterfaces(ctx context.Context, fileReader io.Reader, generic []interface{}) (err error) { + biosFirmwareUpdater := make([]BIOSFirmwareUpdater, 0) + for _, elem := range generic { + switch p := elem.(type) { + case BIOSFirmwareUpdater: + biosFirmwareUpdater = append(biosFirmwareUpdater, p) + default: + e := fmt.Sprintf("not a BIOSFirmwareUpdater implementation: %T", p) + err = multierror.Append(err, errors.New(e)) + } + } + if len(biosFirmwareUpdater) == 0 { + return multierror.Append(err, errors.New("no BIOSFirmwareUpdater implementations found")) + } + + return UpdateBIOSFirmware(ctx, fileReader, biosFirmwareUpdater) +} diff --git a/bmc/firmware_test.go b/bmc/firmware_test.go new file mode 100644 index 00000000..b5459d16 --- /dev/null +++ b/bmc/firmware_test.go @@ -0,0 +1,349 @@ +package bmc + +import ( + "bytes" + "context" + "errors" + "io" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-multierror" +) + +type firmwareTester struct { + MakeNotOK bool + MakeErrorOut bool +} + +func (f *firmwareTester) GetBMCVersion(ctx context.Context) (version string, err error) { + if f.MakeErrorOut { + return "", errors.New("failed to get BMC version") + } + if f.MakeNotOK { + return "", nil + } + return "1.33.7", nil +} + +func (f *firmwareTester) FirmwareUpdateBMC(ctx context.Context, fileReader io.Reader) (err error) { + if f.MakeErrorOut { + return errors.New("failed update") + } + + return nil +} + +func (f *firmwareTester) FirmwareUpdateBIOS(ctx context.Context, fileReader io.Reader) (err error) { + if f.MakeErrorOut { + return errors.New("failed update") + } + return nil +} + +func (f *firmwareTester) GetBIOSVersion(ctx context.Context) (version string, err error) { + if f.MakeErrorOut { + return "", errors.New("failed to get BIOS version") + } + if f.MakeNotOK { + return "", nil + } + return "1.44.7", nil +} + +func TestGetBMCVersion(t *testing.T) { + testCases := []struct { + name string + version string + makeFail bool + err error + ctxTimeout time.Duration + }{ + {name: "success", version: "1.33.7", err: nil}, + {name: "failure", version: "", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("failed to get BMC version"), errors.New("failed to get BMC version")}}}, + {name: "fail context timeout", version: "", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded"), errors.New("failed to get BMC version")}}, ctxTimeout: time.Nanosecond * 1}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + testImplementation := firmwareTester{MakeErrorOut: tc.makeFail} + expectedResult := tc.version + if tc.ctxTimeout == 0 { + tc.ctxTimeout = time.Second * 3 + } + ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) + defer cancel() + result, err := GetBMCVersion(ctx, []BMCVersionGetter{&testImplementation}) + if err != nil { + diff := cmp.Diff(tc.err.Error(), err.Error()) + if diff != "" { + t.Fatal(diff) + } + + } else { + diff := cmp.Diff(expectedResult, result) + if diff != "" { + t.Fatal(diff) + } + } + + }) + } +} + +func TestGetBMCVersionFromInterfaces(t *testing.T) { + testCases := []struct { + name string + version string + err error + badImplementation bool + want string + }{ + {name: "success", version: "1.33.7", err: nil}, + {name: "no implementations found", version: "", want: "", badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a BMCVersionGetter implementation: *struct {}"), errors.New("no BMCVersionGetter implementations found")}}}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var generic []interface{} + if tc.badImplementation { + badImplementation := struct{}{} + generic = []interface{}{&badImplementation} + } else { + testImplementation := firmwareTester{} + generic = []interface{}{&testImplementation} + } + expectedResult := tc.version + result, err := GetBMCVersionFromInterfaces(context.Background(), generic) + if err != nil { + diff := cmp.Diff(tc.err.Error(), err.Error()) + if diff != "" { + t.Fatal(diff) + } + + } else { + diff := cmp.Diff(expectedResult, result) + if diff != "" { + t.Fatal(diff) + } + } + + }) + } +} + +func TestUpdateBMCFirmware(t *testing.T) { + testCases := []struct { + name string + makeFail bool + err error + ctxTimeout time.Duration + }{ + {name: "success", err: nil}, + {name: "failure", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("failed update"), errors.New("failed to update BMC firmware")}}}, + {name: "fail context timeout", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded"), errors.New("failed to update BMC firmware")}}, ctxTimeout: time.Nanosecond * 1}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + testImplementation := firmwareTester{MakeErrorOut: tc.makeFail} + if tc.ctxTimeout == 0 { + tc.ctxTimeout = time.Second * 3 + } + ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) + defer cancel() + err := UpdateBMCFirmware(ctx, bytes.NewReader([]byte(`foo`)), []BMCFirmwareUpdater{&testImplementation}) + if err != nil { + diff := cmp.Diff(tc.err.Error(), err.Error()) + if diff != "" { + t.Fatal(diff) + } + } + }) + } +} + +func TestUpdateBMCFirmwareFromInterfaces(t *testing.T) { + testCases := []struct { + name string + err error + badImplementation bool + want string + }{ + {name: "success", err: nil}, + {name: "no implementations found", want: "", badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a BMCFirmwareUpdater implementation: *struct {}"), errors.New("no BMCFirmwareUpdater implementations found")}}}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var generic []interface{} + if tc.badImplementation { + badImplementation := struct{}{} + generic = []interface{}{&badImplementation} + } else { + testImplementation := firmwareTester{} + generic = []interface{}{&testImplementation} + } + err := UpdateBMCFirmwareFromInterfaces(context.Background(), bytes.NewReader([]byte(`foo`)), generic) + if err != nil { + diff := cmp.Diff(tc.err.Error(), err.Error()) + if diff != "" { + t.Fatal(diff) + } + } + }) + } +} + +func TestGetBIOSVersion(t *testing.T) { + testCases := []struct { + name string + version string + makeFail bool + err error + ctxTimeout time.Duration + }{ + {name: "success", version: "1.44.7", err: nil}, + {name: "failure", version: "", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("failed to get BIOS version"), errors.New("failed to get BIOS version")}}}, + {name: "fail context timeout", version: "", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded"), errors.New("failed to get BIOS version")}}, ctxTimeout: time.Nanosecond * 1}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + testImplementation := firmwareTester{MakeErrorOut: tc.makeFail} + expectedResult := tc.version + if tc.ctxTimeout == 0 { + tc.ctxTimeout = time.Second * 3 + } + ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) + defer cancel() + result, err := GetBIOSVersion(ctx, []BIOSVersionGetter{&testImplementation}) + if err != nil { + diff := cmp.Diff(tc.err.Error(), err.Error()) + if diff != "" { + t.Fatal(diff) + } + + } else { + diff := cmp.Diff(expectedResult, result) + if diff != "" { + t.Fatal(diff) + } + } + + }) + } +} + +func TestGetBIOSVersionFromInterfaces(t *testing.T) { + testCases := []struct { + name string + version string + err error + badImplementation bool + want string + }{ + {name: "success", version: "1.44.7", err: nil}, + {name: "no implementations found", version: "", want: "", badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a BIOSVersionGetter implementation: *struct {}"), errors.New("no BIOSVersionGetter implementations found")}}}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var generic []interface{} + if tc.badImplementation { + badImplementation := struct{}{} + generic = []interface{}{&badImplementation} + } else { + testImplementation := firmwareTester{} + generic = []interface{}{&testImplementation} + } + expectedResult := tc.version + result, err := GetBIOSVersionFromInterfaces(context.Background(), generic) + if err != nil { + diff := cmp.Diff(tc.err.Error(), err.Error()) + if diff != "" { + t.Fatal(diff) + } + + } else { + diff := cmp.Diff(expectedResult, result) + if diff != "" { + t.Fatal(diff) + } + } + + }) + } +} + +func TestUpdateBIOSFirmware(t *testing.T) { + testCases := []struct { + name string + makeFail bool + err error + ctxTimeout time.Duration + }{ + {name: "success", err: nil}, + {name: "failure", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("failed update"), errors.New("failed to update BIOS firmware")}}}, + {name: "fail context timeout", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded"), errors.New("failed to update BIOS firmware")}}, ctxTimeout: time.Nanosecond * 1}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + testImplementation := firmwareTester{MakeErrorOut: tc.makeFail} + if tc.ctxTimeout == 0 { + tc.ctxTimeout = time.Second * 3 + } + ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) + defer cancel() + err := UpdateBIOSFirmware(ctx, bytes.NewReader([]byte(`foo`)), []BIOSFirmwareUpdater{&testImplementation}) + if err != nil { + diff := cmp.Diff(tc.err.Error(), err.Error()) + if diff != "" { + t.Fatal(diff) + } + } + }) + } +} + +func TestUpdateBIOSFirmwareFromInterfaces(t *testing.T) { + testCases := []struct { + name string + err error + badImplementation bool + want string + }{ + {name: "success", err: nil}, + {name: "no implementations found", want: "", badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a BIOSFirmwareUpdater implementation: *struct {}"), errors.New("no BIOSFirmwareUpdater implementations found")}}}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var generic []interface{} + if tc.badImplementation { + badImplementation := struct{}{} + generic = []interface{}{&badImplementation} + } else { + testImplementation := firmwareTester{} + generic = []interface{}{&testImplementation} + } + err := UpdateBIOSFirmwareFromInterfaces(context.Background(), bytes.NewReader([]byte(`foo`)), generic) + if err != nil { + diff := cmp.Diff(tc.err.Error(), err.Error()) + if diff != "" { + t.Fatal(diff) + } + } + }) + } +} diff --git a/client.go b/client.go index 17e0e004..21fcf286 100644 --- a/client.go +++ b/client.go @@ -4,9 +4,11 @@ package bmclib import ( "context" + "io" "github.com/bmc-toolbox/bmclib/bmc" "github.com/bmc-toolbox/bmclib/logging" + "github.com/bmc-toolbox/bmclib/providers/asrockrack" "github.com/bmc-toolbox/bmclib/providers/ipmitool" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" @@ -46,12 +48,12 @@ func NewClient(host, port, user, pass string, opts ...Option) *Client { Logger: logging.DefaultLogger(), Registry: registrar.NewRegistry(), } - defaultClient.Registry.Logger = defaultClient.Logger for _, opt := range opts { opt(defaultClient) } + defaultClient.Registry.Logger = defaultClient.Logger defaultClient.Auth.Host = host defaultClient.Auth.Port = port defaultClient.Auth.User = user @@ -68,6 +70,11 @@ func (c *Client) registerProviders() { // register ipmitool provider driverIpmitool := &ipmitool.Conn{Host: c.Auth.Host, Port: c.Auth.Port, User: c.Auth.User, Pass: c.Auth.Pass, Log: c.Logger} c.Registry.Register(ipmitool.ProviderName, ipmitool.ProviderProtocol, ipmitool.Features, nil, driverIpmitool) + + // register ASRR vendorapi provider + driverAsrockrack, _ := asrockrack.New(c.Auth.Host, c.Auth.User, c.Auth.Pass, c.Logger) + c.Registry.Register(asrockrack.ProviderName, asrockrack.ProviderProtocol, asrockrack.Features, nil, driverAsrockrack) + } // Open pass through to library function @@ -119,3 +126,23 @@ func (c *Client) SetBootDevice(ctx context.Context, bootDevice string, setPersis func (c *Client) ResetBMC(ctx context.Context, resetType string) (ok bool, err error) { return bmc.ResetBMCFromInterfaces(ctx, resetType, c.Registry.GetDriverInterfaces()) } + +// GetBMCVersion pass through library function +func (c *Client) GetBMCVersion(ctx context.Context) (version string, err error) { + return bmc.GetBMCVersionFromInterfaces(ctx, c.Registry.GetDriverInterfaces()) +} + +// UpdateBMCFirmware pass through library function +func (c *Client) UpdateBMCFirmware(ctx context.Context, fileReader io.Reader) (err error) { + return bmc.UpdateBMCFirmwareFromInterfaces(ctx, fileReader, c.Registry.GetDriverInterfaces()) +} + +// GetBIOSVersion pass through library function +func (c *Client) GetBIOSVersion(ctx context.Context) (version string, err error) { + return bmc.GetBIOSVersionFromInterfaces(ctx, c.Registry.GetDriverInterfaces()) +} + +// UpdateBIOSFirmware pass through library function +func (c *Client) UpdateBIOSFirmware(ctx context.Context, fileReader io.Reader) (err error) { + return bmc.UpdateBIOSFirmwareFromInterfaces(ctx, fileReader, c.Registry.GetDriverInterfaces()) +} diff --git a/discover/discover.go b/discover/discover.go index 7c4473e4..c01a95d3 100644 --- a/discover/discover.go +++ b/discover/discover.go @@ -22,6 +22,7 @@ const ( ProbeM1000e = "m1000e" ProbeQuanta = "quanta" ProbeHpCl100 = "hpcl100" + ProbeASRockRack = "asrockrack" ) // ScanAndConnect will scan the bmc trying to learn the device type and return a working connection. @@ -74,6 +75,7 @@ func ScanAndConnect(host string, username string, password string, options ...Op ProbeM1000e, ProbeQuanta, ProbeHpCl100, + ProbeASRockRack, } if opts.Hint != "" { diff --git a/discover/discovery_test.go b/discover/discover_test.go similarity index 100% rename from discover/discovery_test.go rename to discover/discover_test.go diff --git a/examples/main.go b/examples/main.go index 9850c328..dd4b99fe 100644 --- a/examples/main.go +++ b/examples/main.go @@ -21,9 +21,9 @@ import ( // github.com/wojas/genericr: genericr func main() { - ip := "" - user := "user" - pass := "password" + ip := "" + user := "admin" + pass := "admin" logger := logrus.New() logger.SetLevel(logrus.DebugLevel) @@ -37,7 +37,7 @@ func main() { printStatus(conn, logger) logger.Info("printing status with the default builtin logger") - os.Setenv("BMCLIB_LOG_LEVEL", "debug") + os.Setenv("BMCLIB_LOG_LEVEL", "trace") conn, err = withDefaultBuiltinLogger(ip, user, pass) if err != nil { logger.Fatal(err) diff --git a/examples/pre.v1/main.go b/examples/pre.v1/main.go new file mode 100644 index 00000000..d5f024ff --- /dev/null +++ b/examples/pre.v1/main.go @@ -0,0 +1,58 @@ +package main + +// This snippet utilizes the older bmclib interface methods +// it connects to the bmc and retries its version + +import ( + "context" + "fmt" + "os" + + "github.com/bmc-toolbox/bmclib/devices" + "github.com/bmc-toolbox/bmclib/discover" + "github.com/bombsimon/logrusr" + "github.com/sirupsen/logrus" +) + +func main() { + //ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx := context.TODO() + //defer cancel() + host := "" + user := "" + pass := "" + + l := logrus.New() + l.Level = logrus.TraceLevel + logger := logrusr.NewLogger(l) + + c, err := discover.ScanAndConnect( + host, + user, + pass, + discover.WithContext(ctx), + discover.WithLogger(logger), + ) + + if err != nil { + logger.Error(err, "Error connecting to bmc") + } + + bmc := c.(devices.Bmc) + + err = bmc.CheckCredentials() + if err != nil { + logger.Error(err, "Failed to validate credentials") + os.Exit(1) + } + + defer bmc.Close(ctx) + + s, err := bmc.Serial() + if err != nil { + logger.Error(err, "Error getting bmc serial") + os.Exit(1) + } + fmt.Println(s) + +} diff --git a/examples/v1/main.go b/examples/v1/main.go new file mode 100644 index 00000000..ee205a92 --- /dev/null +++ b/examples/v1/main.go @@ -0,0 +1,60 @@ +package main + +/* + This utilizes what is to tbe the 'v1' bmclib interface methods to flash a firmware image +*/ + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/bmc-toolbox/bmclib" + "github.com/bombsimon/logrusr" + "github.com/sirupsen/logrus" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + host := "" + port := "" + user := "" + pass := "" + + l := logrus.New() + l.Level = logrus.DebugLevel + logger := logrusr.NewLogger(l) + + var err error + + cl := bmclib.NewClient(host, port, user, pass, bmclib.WithLogger(logger)) + err = cl.Open(ctx) + if err != nil { + log.Fatal(err, "bmc login failed") + } + + defer cl.Close(ctx) + + v, err := cl.GetBMCVersion(ctx) + if err != nil { + log.Fatal(err, "unable to retrieve BMC version") + } + + fmt.Println("BMC version: " + v) + + // open file handle + fh, err := os.Open("/tmp/E3C246D4I-NL_L0.03.00.ima") + if err != nil { + log.Fatal(err) + } + defer fh.Close() + + err = cl.UpdateBMCFirmware(ctx, fh) + if err != nil { + log.Fatal(err) + } + +} diff --git a/examples/v2/main.go b/examples/v2/main.go new file mode 100644 index 00000000..1ab86c55 --- /dev/null +++ b/examples/v2/main.go @@ -0,0 +1,60 @@ +package main + +/* + This utilizes the 'v2' bmclib interface methods to flash a firmware image +*/ + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/bmc-toolbox/bmclib" + "github.com/bombsimon/logrusr" + "github.com/sirupsen/logrus" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + host := "" + port := "" + user := "" + pass := "" + + l := logrus.New() + l.Level = logrus.DebugLevel + logger := logrusr.NewLogger(l) + + var err error + + cl := bmclib.NewClient(host, port, user, pass, bmclib.WithLogger(logger)) + err = cl.Open(ctx) + if err != nil { + log.Fatal(err, "bmc login failed") + } + + defer cl.Close(ctx) + + v, err := cl.GetBMCVersion(ctx) + if err != nil { + log.Fatal(err, "unable to retrieve BMC version") + } + + fmt.Println("BMC version: " + v) + + // open file handle + fh, err := os.Open("/tmp/E3C246D4I-NL_L0.03.00.ima") + if err != nil { + log.Fatal(err) + } + defer fh.Close() + + err = cl.UpdateBMCFirmware(ctx, fh) + if err != nil { + log.Fatal(err) + } + +} diff --git a/go.mod b/go.mod index 0130060a..78ae11e3 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( golang.org/x/net v0.0.0-20210119194325-5f4716e94777 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect golang.org/x/text v0.3.5 // indirect - gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/providers/asrockrack/asrockrack.go b/providers/asrockrack/asrockrack.go new file mode 100644 index 00000000..ad897dd3 --- /dev/null +++ b/providers/asrockrack/asrockrack.go @@ -0,0 +1,285 @@ +package asrockrack + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "time" + + "github.com/bmc-toolbox/bmclib/internal/httpclient" + "github.com/bmc-toolbox/bmclib/providers" + "github.com/go-logr/logr" + "github.com/jacobweinstock/registrar" +) + +const ( + // ProviderName for the provider implementation + ProviderName = "asrockrack" + // ProviderProtocol for the provider implementation + ProviderProtocol = "vendorapi" +) + +var ( + // Features implemented by asrockrack https + Features = registrar.Features{ + providers.FeatureBiosVersionRead, + providers.FeatureBmcVersionRead, + providers.FeatureBiosFirmwareUpdate, + providers.FeatureBmcFirmwareUpdate, + } +) + +// ASRockRack holds the status and properties of a connection to a asrockrack bmc +type ASRockRack struct { + ip string + username string + password string + loginSession *loginSession + httpClient *http.Client + fwInfo *firmwareInfo + resetRequired bool // Indicates if the BMC requires a reset + skipLogout bool // A Close() / httpsLogout() request is ignored if the BMC was just flashed - since the sessions are terminated either way + log logr.Logger +} + +// New returns a new ASRockRack instance ready to be used +func New(ip string, username string, password string, log logr.Logger) (*ASRockRack, error) { + + client, err := httpclient.Build() + if err != nil { + return nil, err + } + + return &ASRockRack{ + ip: ip, + username: username, + password: password, + log: log, + loginSession: &loginSession{}, + httpClient: client, + }, nil +} + +// Compatible implements the registrar.Verifier interface +// returns true if the BMC is identified to be an asrockrack +func (a *ASRockRack) Compatible() bool { + + resp, statusCode, err := a.queryHTTPS("/", "GET", nil, nil) + if err != nil { + return false + } + + if statusCode != 200 { + return false + } + + return bytes.Contains(resp, []byte(`ASRockRack`)) +} + +// Open a connection to a BMC, implements the Opener interface +func (a *ASRockRack) Open(ctx context.Context) (err error) { + return a.httpsLogin() +} + +// Close a connection to a BMC, implements the Closer interface +func (a *ASRockRack) Close(ctx context.Context) (err error) { + + if a.skipLogout { + return nil + } + + return a.httpsLogout() +} + +// CheckCredentials verify whether the credentials are valid or not +func (a *ASRockRack) CheckCredentials() (err error) { + return a.httpsLogin() +} + +// BiosVersion returns the BIOS version from the BMC +func (a *ASRockRack) GetBIOSVersion(ctx context.Context) (string, error) { + + var err error + if a.fwInfo == nil { + a.fwInfo, err = a.firmwareInfo() + if err != nil { + return "", err + } + } + + return a.fwInfo.BIOSVersion, nil +} + +// BMCVersion returns the BMC version +func (a *ASRockRack) GetBMCVersion(ctx context.Context) (string, error) { + + var err error + if a.fwInfo == nil { + a.fwInfo, err = a.firmwareInfo() + if err != nil { + return "", err + } + } + + return a.fwInfo.BMCVersion, nil +} + +// nolint: gocyclo +// BMC firmware update is a multi step process +// this method initiates the upgrade process and waits in a loop until the device has been upgraded +func (a *ASRockRack) FirmwareUpdateBMC(ctx context.Context, fileReader io.Reader) error { + + defer func() { + // The device needs to be reset to be removed from flash mode, + // this is required once setFlashMode() is invoked. + // The BMC resets itself once a firmware flash is successful or failed. + if a.resetRequired { + a.log.V(1).Info("info", "resetting BMC, this takes a few minutes") + err := a.reset() + if err != nil { + a.log.Error(err, "failed to reset BMC") + } + } + }() + + var err error + + // 1. set the device to flash mode - prepares the flash + a.log.V(1).Info("info", "action", "set device to flash mode, takes a minute...", "step", "1/5") + err = a.setFlashMode() + if err != nil { + return fmt.Errorf("failed in step 1/5 - set device to flash mode: " + err.Error()) + } + + // 2. upload firmware image file + a.log.V(1).Info("info", "action", "upload BMC firmware image", "step", "2/5") + err = a.uploadFirmware("api/maintenance/firmware", fileReader) + if err != nil { + return fmt.Errorf("failed in step 2/5 - upload BMC firmware image: " + err.Error()) + } + + // 3. BMC to verify the uploaded file + err = a.verifyUploadedFirmware() + a.log.V(1).Info("info", "action", "BMC verify uploaded firmware", "step", "3/5") + if err != nil { + return fmt.Errorf("failed in step 3/5 - BMC verify uploaded firmware: " + err.Error()) + } + + startTS := time.Now() + // 4. Run the upgrade - preserving current config + a.log.V(1).Info("info", "action", "proceed with upgrade, preserve current configuration", "step", "4/5") + err = a.upgradeBMC() + if err != nil { + return fmt.Errorf("failed in step 4/5 - proceed with upgrade: " + err.Error()) + } + + // progress check interval + progressT := time.NewTicker(2 * time.Second).C + // timeout interval + timeoutT := time.NewTicker(30 * time.Minute).C + maxErrors := 20 + var errorsCount int + + // 5.loop until firmware was updated - with a timeout + for { + select { + case <-progressT: + // check progress + endpoint := "api/maintenance/firmware/flash-progress" + p, err := a.flashProgress(endpoint) + if err != nil { + errorsCount++ + a.log.V(1).Error(err, "step", "5/5 - error checking flash progress", "error count", errorsCount, "max errors", maxErrors, "elapsed time", time.Since(startTS).String()) + continue + } + + a.log.V(1).Info("info", "action", p.Action, "step", "5/5", "progress", p.Progress, "elapsed time", time.Since(startTS).String()) + + // all done! + if p.State == 2 { + a.log.V(1).Info("info", "action", "flash process complete", "step", "5/5", "progress", p.Progress, "elapsed time", time.Since(startTS).String()) + // The BMC resets by itself after a successful flash + a.resetRequired = false + // HTTP sessions are terminated once the BMC resets after an upgrade + a.skipLogout = true + return nil + } + case <-timeoutT: + return fmt.Errorf("timeout in step 5/5 - flash progress, error count: %d, elapsed time: %s", errorsCount, time.Since(startTS).String()) + } + } +} + +func (a *ASRockRack) FirmwareUpdateBIOS(ctx context.Context, fileReader io.Reader) error { + + defer func() { + if a.resetRequired { + a.log.V(1).Info("info", "resetting BMC, this takes a few minutes") + err := a.reset() + if err != nil { + a.log.Error(err, "failed to reset BMC") + } + } + }() + + var err error + + // 1. upload firmware image file + a.log.V(1).Info("info", "action", "upload BIOS firmware image", "step", "1/4") + err = a.uploadFirmware("api/asrr/maintenance/BIOS/firmware", fileReader) + if err != nil { + return fmt.Errorf("failed in step 1/4 - upload firmware image: " + err.Error()) + } + + // 2. set update parameters to preserve configuratin + a.log.V(1).Info("info", "action", "set flash configuration", "step", "2/4") + err = a.biosUpgradeConfiguration() + if err != nil { + return fmt.Errorf("failed in step 2/4 - set flash configuration: " + err.Error()) + } + + startTS := time.Now() + // 3. run upgrade + a.log.V(1).Info("info", "action", "proceed with upgrade", "step", "3/4") + err = a.biosUpgrade() + if err != nil { + return fmt.Errorf("failed in step 3/4 - proceed with upgrade: " + err.Error()) + } + + // progress check interval + progressT := time.NewTicker(2 * time.Second).C + // timeout interval + timeoutT := time.NewTicker(30 * time.Minute).C + maxErrors := 20 + var errorsCount int + + // 5.loop until firmware was updated - with a timeout + for { + select { + case <-progressT: + // check progress + endpoint := "api/asrr/maintenance/BIOS/flash-progress" + p, err := a.flashProgress(endpoint) + if err != nil { + errorsCount++ + a.log.V(1).Error(err, "action", "check flash progress", "step", "4/4", "error count", errorsCount, "max errors", maxErrors, "elapsed time", time.Since(startTS).String()) + continue + } + + a.log.V(1).Info("info", "action", "check flash progress", "step", "4/4", "progress", p.Progress, "action", p.Action, "elapsed time", time.Since(startTS).String()) + + // all done! + if p.State == 2 { + a.log.V(1).Info("info", "action", "flash process complete", "step", "4/4", "progress", p.Progress, "elapsed time", time.Since(startTS).String()) + // Reset BMC after flash + a.resetRequired = true + return nil + } + case <-timeoutT: + return fmt.Errorf("timeout in step 4/4 - flash progress, error count: %d, elapsed time: %s", errorsCount, time.Since(startTS).String()) + } + } + +} diff --git a/providers/asrockrack/asrockrack_test.go b/providers/asrockrack/asrockrack_test.go new file mode 100644 index 00000000..d363ba49 --- /dev/null +++ b/providers/asrockrack/asrockrack_test.go @@ -0,0 +1,91 @@ +package asrockrack + +import ( + "context" + "os" + "testing" + + "gopkg.in/go-playground/assert.v1" +) + +func Test_Compatible(t *testing.T) { + b := aClient.Compatible() + if !b { + t.Errorf("expected true, got false") + } +} + +func Test_httpLogin(t *testing.T) { + + err := aClient.httpsLogin() + if err != nil { + t.Errorf(err.Error()) + } + + assert.Equal(t, "l5L29IP7", aClient.loginSession.CSRFToken) +} + +func Test_Close(t *testing.T) { + + err := aClient.httpsLogin() + if err != nil { + t.Errorf(err.Error()) + } + + err = aClient.httpsLogout() + if err != nil { + t.Errorf(err.Error()) + } +} + +func Test_FirmwareInfo(t *testing.T) { + + expected := firmwareInfo{ + BMCVersion: "0.01.00", + BIOSVersion: "L2.07B", + MEVersion: "5.1.3.78", + MicrocodeVersion: "000000ca", + CPLDVersion: "N/A", + CMVersion: "0.13.01", + BPBVersion: "0.0.002.0", + NodeID: "2", + } + + err := aClient.httpsLogin() + if err != nil { + t.Errorf(err.Error()) + } + + fwInfo, err := aClient.firmwareInfo() + if err != nil { + t.Error(err.Error()) + } + + assert.Equal(t, expected, fwInfo) + +} + +func Test_FirwmwareUpdateBMC(t *testing.T) { + + err := aClient.httpsLogin() + if err != nil { + t.Errorf(err.Error()) + } + + upgradeFile := "/tmp/dummy-E3C246D4I-NL_L0.01.00.ima" + _, err = os.Create(upgradeFile) + if err != nil { + t.Errorf(err.Error()) + } + + fh, err := os.Open(upgradeFile) + if err != nil { + t.Errorf(err.Error()) + } + + defer fh.Close() + err = aClient.FirmwareUpdateBMC(context.TODO(), fh) + if err != nil { + t.Errorf(err.Error()) + } +} diff --git a/providers/asrockrack/firmware_update.md b/providers/asrockrack/firmware_update.md new file mode 100644 index 00000000..03b28b78 --- /dev/null +++ b/providers/asrockrack/firmware_update.md @@ -0,0 +1,61 @@ +### BMC + Flashing a BMC firmware seems to be a multi step process + + + 1. PUT /api/maintenance/flash + no payload (seems to set the device to be in flash mode or such) + 200 OK - takes about a minute to return + + 2. POST /api/maintenance/firmware + Content-Type: multipart/form-data + ------WebKitFormBoundaryESKCgdjyLnqUPHBK + Content-Disposition: form-data; name="fwimage"; filename="E3C246D4I-NL_L0.01.00.ima" + Content-Type: application/octet-stream + ------WebKitFormBoundaryESKCgdjyLnqUPHBK-- + . response - '{"cc": 0}' - successful upload + + 3. GET /api/maintenance/firmware/verification + 500 - Bad firmware payload -> invoke reset + 200 - OK + [ { "id": 1, "current_image_name": "ast2500e", "current_image_version1": "0.01.00", "current_image_version2": "", "new_image_version": "0.03.00", "section_status": 0, "verification_status": 5 } ] + + 4. If verificaion fails OR firmware update progress is at 100% done - invoke reset + + GET /api/maintenance/reset + 200 OK + + 5. PUT /api/maintenance/firmware/upgrade + payload {"preserve_config":1,"preserve_network":0,"preserve_user":0,"flash_status":1} + 200 OK + response - same as payload + + 6. GET https://10.230.148.171/api/maintenance/firmware/flash-progress + { "id": 1, "action": "Flashing...", "progress": "12% done ", "state": 0 } + { "id": 1, "action": "Flashing...", "progress": "100% done", "state": 0 } + + +### BIOS + +1. POST api/asrr/maintenance/BIOS/firmware + multipart payload: + +------WebKitFormBoundaryBet48KCtZK4gBlQz +Content-Disposition: form-data; name="fwimage"; filename="E6D4INL2.07B" +Content-Type: application/octet-stream + + +------WebKitFormBoundaryBet48KCtZK4gBlQz-- + + +2. POST api/asrr/maintenance/BIOS/configuration + payload {"action":"2"} + 200 OK + {"response": 1} + +3. POST api/asrr/maintenance/BIOS/upgrade + payload {action: 3} + 200 oK + +4. GET api/asrr/maintenance/BIOS/flash-progress + + diff --git a/providers/asrockrack/helpers.go b/providers/asrockrack/helpers.go new file mode 100644 index 00000000..98eaab49 --- /dev/null +++ b/providers/asrockrack/helpers.go @@ -0,0 +1,396 @@ +package asrockrack + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/http/httputil" + + "github.com/bmc-toolbox/bmclib/errors" +) + +// API session setup response payload +type loginSession struct { + CSRFToken string `json:"csrftoken,omitempty"` + Privilege int `json:"privilege,omitempty"` + RACSessionID int `json:"racsession_id,omitempty"` + ExtendedPrivilege int `json:"extendedpriv,omitempty"` +} + +// Firmware info endpoint response payload +type firmwareInfo struct { + BMCVersion string `json:"BMC_fw_version"` + BIOSVersion string `json:"BIOS_fw_version"` + MEVersion string `json:"ME_fw_version"` + MicrocodeVersion string `json:"Micro_Code_version"` + CPLDVersion string `json:"CPLD_version"` + CMVersion string `json:"CM_version"` + BPBVersion string `json:"BPB_version"` + NodeID string `json:"Node_id"` +} + +// Payload to preseve config when updating the BMC firmware +type preserveConfig struct { + FlashStatus int `json:"flash_status"` // 1 = full firmware flash, 2 = section based flash, 3 - version compare flash + PreserveConfig int `json:"preserve_config"` + PreserveNetwork int `json:"preserve_network"` + PreserveUser int `json:"preserve_user"` +} + +// Firmware flash progress +//{ "id": 1, "action": "Flashing...", "progress": "12% done ", "state": 0 } +//{ "id": 1, "action": "Flashing...", "progress": "100% done", "state": 0 } +type upgradeProgress struct { + ID int `json:"id,omitempty"` + Action string `json:"action,omitempty"` + Progress string `json:"progress,omitempty"` + State int `json:"state,omitempty"` +} + +// BIOS upgrade commands +// 2 == configure +// 3 == apply upgrade +type biosUpdateAction struct { + Action int `json:"action"` +} + +// 1 Set BMC to flash mode and prepare flash area +// at this point all logged in sessions are terminated +// and no logins are permitted +func (a *ASRockRack) setFlashMode() error { + + endpoint := "api/maintenance/flash" + + _, statusCode, err := a.queryHTTPS(endpoint, "PUT", nil, nil) + if err != nil { + return err + } + + if statusCode != 200 { + return fmt.Errorf("non 200 response: %d", statusCode) + } + + a.resetRequired = true + + return nil +} + +// 2 Upload the firmware file +func (a *ASRockRack) uploadFirmware(endpoint string, fwReader io.Reader) error { + + // setup a buffer for our multipart form + var form bytes.Buffer + w := multipart.NewWriter(&form) + + // create form data from update image + fwWriter, err := w.CreateFormFile("fwimage", "image") + if err != nil { + return err + } + + // copy file contents into form payload + _, err = io.Copy(fwWriter, fwReader) + if err != nil { + return err + } + + // multi-part content type + headers := map[string]string{"Content-Type": w.FormDataContentType()} + + // close multipart writer - adds the teminating boundary. + w.Close() + + // POST payload + _, statusCode, err := a.queryHTTPS(endpoint, "POST", form.Bytes(), headers) + if err != nil { + return err + } + + if statusCode != 200 { + return fmt.Errorf("non 200 response: %d", statusCode) + } + + return nil +} + +// 3. Verify uploaded firmware file - to be invoked after uploadFirmware() +func (a *ASRockRack) verifyUploadedFirmware() error { + + endpoint := "api/maintenance/firmware/verification" + + _, statusCode, err := a.queryHTTPS(endpoint, "GET", nil, nil) + if err != nil { + return err + } + + if statusCode != 200 { + return fmt.Errorf("non 200 response: %d", statusCode) + } + + return nil + +} + +// 4. Start firmware flashing process - to be invoked after verifyUploadedFirmware +func (a *ASRockRack) upgradeBMC() error { + + endpoint := "api/maintenance/firmware/upgrade" + + // preserve all configuration during upgrade, full flash + pConfig := &preserveConfig{PreserveConfig: 1, FlashStatus: 1} + payload, err := json.Marshal(pConfig) + if err != nil { + return err + } + + headers := map[string]string{"Content-Type": "application/json"} + _, statusCode, err := a.queryHTTPS(endpoint, "PUT", payload, headers) + if err != nil { + return err + } + + if statusCode != 200 { + return fmt.Errorf("non 200 response: %d", statusCode) + } + + return nil + +} + +// 4. reset BMC +func (a *ASRockRack) reset() error { + + endpoint := "api/maintenance/reset" + + _, statusCode, err := a.queryHTTPS(endpoint, "POST", nil, nil) + if err != nil { + return err + } + + if statusCode != 200 { + return fmt.Errorf("non 200 response: %d", statusCode) + } + + return nil + +} + +// 5. firmware flash progress +func (a *ASRockRack) flashProgress(endpoint string) (*upgradeProgress, error) { + + resp, statusCode, err := a.queryHTTPS(endpoint, "GET", nil, nil) + if err != nil { + return nil, err + } + + if statusCode != 200 { + return nil, fmt.Errorf("non 200 response: %d", statusCode) + } + + p := &upgradeProgress{} + err = json.Unmarshal(resp, p) + if err != nil { + return nil, err + } + + return p, nil + +} + +// Query firmware information from the BMC +func (a *ASRockRack) firmwareInfo() (*firmwareInfo, error) { + + endpoint := "api/asrr/fw-info" + + resp, statusCode, err := a.queryHTTPS(endpoint, "GET", nil, nil) + if err != nil { + return nil, err + } + + if statusCode != 200 { + return nil, fmt.Errorf("non 200 response: %d", statusCode) + } + + f := &firmwareInfo{} + err = json.Unmarshal(resp, f) + if err != nil { + return nil, err + } + + return f, nil + +} + +// Set the BIOS upgrade configuration +// - preserve current configuration +func (a *ASRockRack) biosUpgradeConfiguration() error { + + endpoint := "api/asrr/maintenance/BIOS/configuration" + + // Preserve existing configuration? + p := biosUpdateAction{Action: 2} + payload, err := json.Marshal(p) + if err != nil { + return err + } + + headers := map[string]string{"Content-Type": "application/json"} + resp, statusCode, err := a.queryHTTPS(endpoint, "POST", payload, headers) + if err != nil { + return err + } + + if statusCode != 200 { + return fmt.Errorf("non 200 response: %d", statusCode) + } + + f := &firmwareInfo{} + err = json.Unmarshal(resp, f) + if err != nil { + return err + } + + return nil + +} + +// Run BIOS upgrade +func (a *ASRockRack) biosUpgrade() error { + + endpoint := "api/asrr/maintenance/BIOS/upgrade" + + // Run upgrade + p := biosUpdateAction{Action: 3} + payload, err := json.Marshal(p) + if err != nil { + return err + } + + headers := map[string]string{"Content-Type": "application/json"} + resp, statusCode, err := a.queryHTTPS(endpoint, "POST", payload, headers) + if err != nil { + return err + } + + if statusCode != 200 { + return fmt.Errorf("non 200 response: %d", statusCode) + } + + f := &firmwareInfo{} + err = json.Unmarshal(resp, f) + if err != nil { + return err + } + + return nil + +} + +// Aquires a session id cookie and a csrf token +func (a *ASRockRack) httpsLogin() error { + + urlEndpoint := "api/session" + + // login payload + payload := []byte( + fmt.Sprintf("username=%s&password=%s&certlogin=0", + a.username, + a.password, + ), + ) + + headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} + + resp, statusCode, err := a.queryHTTPS(urlEndpoint, "POST", payload, headers) + if err != nil { + return fmt.Errorf("Error logging in: " + err.Error()) + } + + if statusCode == 401 { + return errors.ErrLoginFailed + } + + // Unmarshal login session + err = json.Unmarshal(resp, a.loginSession) + if err != nil { + return fmt.Errorf("error unmarshalling response payload: " + err.Error()) + } + + return nil +} + +// Close ends the BMC session +func (a *ASRockRack) httpsLogout() error { + + urlEndpoint := "api/session" + + _, statusCode, err := a.queryHTTPS(urlEndpoint, "DELETE", nil, nil) + if err != nil { + return fmt.Errorf("Error logging out: " + err.Error()) + } + + if err != nil { + return fmt.Errorf("Error logging out: " + err.Error()) + } + + if statusCode != 200 { + return fmt.Errorf("non 200 response at https logout: %d", statusCode) + } + + return nil +} + +// queryHTTPS run the HTTPS query passing in the required headers +// the / suffix should be excluded from the URLendpoint +// returns - response body, http status code, error if any +func (a *ASRockRack) queryHTTPS(URLendpoint, method string, payload []byte, headers map[string]string) ([]byte, int, error) { + + var body []byte + var err error + var req *http.Request + + URL := fmt.Sprintf("https://%s/%s", a.ip, URLendpoint) + if len(payload) > 0 { + req, err = http.NewRequest(method, URL, bytes.NewReader(payload)) + } else { + req, err = http.NewRequest(method, URL, nil) + } + + // + if err != nil { + return nil, 0, err + } + + // add headers + req.Header.Add("X-CSRFTOKEN", a.loginSession.CSRFToken) + for k, v := range headers { + req.Header.Add(k, v) + } + + // debug dump request + reqDump, _ := httputil.DumpRequestOut(req, true) + a.log.V(3).Info("trace", "url", URL, "requestDump", string(reqDump)) + + resp, err := a.httpClient.Do(req) + if err != nil { + return body, 0, err + } + + // debug dump response + respDump, _ := httputil.DumpResponse(resp, true) + a.log.V(3).Info("trace", "responseDump", string(respDump)) + + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + return body, 0, err + } + + defer resp.Body.Close() + + return body, resp.StatusCode, nil + +} diff --git a/providers/asrockrack/mock_test.go b/providers/asrockrack/mock_test.go new file mode 100644 index 00000000..28a1d89b --- /dev/null +++ b/providers/asrockrack/mock_test.go @@ -0,0 +1,256 @@ +package asrockrack + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strconv" + "strings" + "testing" + + "github.com/bombsimon/logrusr" + "github.com/sirupsen/logrus" +) + +var ( + loginPayload = []byte(`username=foo&password=bar&certlogin=0`) + loginResponse = []byte(`{ "ok": 0, "privilege": 4, "extendedpriv": 259, "racsession_id": 10, "remote_addr": "136.144.50.145", "server_name": "10.230.148.171", "server_addr": "10.230.148.171", "HTTPSEnabled": 1, "CSRFToken": "l5L29IP7" }`) + fwinfoResponse = []byte(`{ "BMC_fw_version": "0.01.00", "BIOS_fw_version": "L2.07B", "ME_fw_version": "5.1.3.78", "Micro_Code_version": "000000ca", "CPLD_version": "N\/A", "CM_version": "0.13.01", "BPB_version": "0.0.002.0", "Node_id": "2" }`) + fwUploadResponse = []byte(`{"cc": 0}`) + fwVerificationResponse = []byte(`[ { "id": 1, "current_image_name": "ast2500e", "current_image_version1": "0.01.00", "current_image_version2": "", "new_image_version": "0.03.00", "section_status": 0, "verification_status": 5 } ]`) + fwUpgradeProgress = []byte(`{ "id": 1, "action": "Flashing...", "progress": "__PERCENT__% done ", "state": __STATE__ }`) +) + +// setup test BMC +var server *httptest.Server +var bmcURL *url.URL +var fwUpgradeState *testFwUpgradeState + +type testFwUpgradeState struct { + FlashModeSet bool + FirmwareUploaded bool + FirmwareVerified bool + UpgradeInitiated bool + UpgradePercent int + ResetDone bool +} + +// the bmc lib client +var aClient *ASRockRack + +func TestMain(m *testing.M) { + + var err error + // setup mock server + server = mockASRockBMC() + bmcURL, _ = url.Parse(server.URL) + + l := logrus.New() + l.Level = logrus.DebugLevel + // setup bmc client + tLog := logrusr.NewLogger(l) + aClient, err = New(bmcURL.Host, "foo", "bar", tLog) + if err != nil { + log.Fatal(err.Error()) + } + + // firmware update test state + fwUpgradeState = &testFwUpgradeState{} + os.Exit(m.Run()) +} + +/////////////// mock bmc service /////////////////////////// +func mockASRockBMC() *httptest.Server { + handler := http.NewServeMux() + handler.HandleFunc("/", index) + handler.HandleFunc("/api/session", session) + handler.HandleFunc("/api/asrr/fw-info", fwinfo) + + // fw update endpoints - in order of invocation + handler.HandleFunc("/api/maintenance/flash", bmcFirmwareUpgrade) + handler.HandleFunc("/api/maintenance/firmware", bmcFirmwareUpgrade) + handler.HandleFunc("/api/maintenance/firmware/verification", bmcFirmwareUpgrade) + handler.HandleFunc("/api/maintenance/firmware/upgrade", bmcFirmwareUpgrade) + handler.HandleFunc("/api/maintenance/firmware/flash-progress", bmcFirmwareUpgrade) + handler.HandleFunc("/api/maintenance/reset", bmcFirmwareUpgrade) + handler.HandleFunc("/api/asrr/maintenance/BIOS/firmware", biosFirmwareUpgrade) + + return httptest.NewTLSServer(handler) +} + +func index(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + _, _ = w.Write([]byte(`ASRockRack`)) + } +} + +func biosFirmwareUpgrade(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%s -> %s\n", r.Method, r.RequestURI) + switch r.Method { + case "POST": + switch r.RequestURI { + case "/api/asrr/maintenance/BIOS/firmware": + + // validate content type + if !strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") { + w.WriteHeader(http.StatusBadRequest) + } + + // parse multipart form + err := r.ParseMultipartForm(100) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + } + } + } +} + +func bmcFirmwareUpgrade(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%s -> %s\n", r.Method, r.RequestURI) + switch r.Method { + case "GET": + switch r.RequestURI { + // 3. bmc verifies uploaded firmware image + case "/api/maintenance/firmware/verification": + if !fwUpgradeState.FirmwareUploaded { + w.WriteHeader(http.StatusBadRequest) + } + fwUpgradeState.FirmwareVerified = true + _, _ = w.Write(fwVerificationResponse) + // 5. flash progress + case "/api/maintenance/firmware/flash-progress": + if !fwUpgradeState.UpgradeInitiated { + w.WriteHeader(http.StatusBadRequest) + } + + resp := fwUpgradeProgress + if fwUpgradeState.UpgradePercent >= 100 { + fwUpgradeState.UpgradePercent = 100 + // state: 2 indicates firmware flash complete + resp = bytes.Replace(resp, []byte("__STATE__"), []byte(strconv.Itoa(2)), 1) + } else { + // state: 0 indicates firmware flash in progress + resp = bytes.Replace(resp, []byte("__STATE__"), []byte(strconv.Itoa(0)), 1) + fwUpgradeState.UpgradePercent += 50 + } + + resp = bytes.Replace(resp, []byte("__PERCENT__"), []byte(strconv.Itoa(fwUpgradeState.UpgradePercent)), 1) + _, _ = w.Write(resp) + } + case "PUT": + + switch r.RequestURI { + // 1. set device to flash mode + case "/api/maintenance/flash": + fwUpgradeState.FlashModeSet = true + w.WriteHeader(http.StatusOK) + // 4. run the upgrade + case "/api/maintenance/firmware/upgrade": + if !fwUpgradeState.FirmwareVerified { + w.WriteHeader(http.StatusBadRequest) + } + + if r.Header.Get("Content-Type") != "application/json" { + w.WriteHeader(http.StatusBadRequest) + } + + p := &preserveConfig{} + err := json.NewDecoder(r.Body).Decode(&p) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // config should be preserved + if p.PreserveConfig != 1 { + w.WriteHeader(http.StatusBadRequest) + } + + // full firmware flash + if p.FlashStatus != 1 { + w.WriteHeader(http.StatusBadRequest) + } + + fwUpgradeState.UpgradeInitiated = true + // respond with request body + b := new(bytes.Buffer) + _, _ = b.ReadFrom(r.Body) + _, _ = w.Write(b.Bytes()) + } + case "POST": + switch r.RequestURI { + case "/api/maintenance/reset": + w.WriteHeader(http.StatusOK) + + // 2. upload firmware + case "/api/maintenance/firmware": + + // validate flash mode set + if !fwUpgradeState.FlashModeSet { + w.WriteHeader(http.StatusBadRequest) + } + + // validate content type + if !strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") { + w.WriteHeader(http.StatusBadRequest) + } + + // parse multipart form + err := r.ParseMultipartForm(100) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + } + + fwUpgradeState.FirmwareUploaded = true + _, _ = w.Write(fwUploadResponse) + } + default: + w.WriteHeader(http.StatusBadRequest) + } +} + +func fwinfo(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + _, _ = w.Write(fwinfoResponse) + } + +} + +func session(w http.ResponseWriter, r *http.Request) { + + switch r.Method { + case "POST": + // login to BMC + b, _ := ioutil.ReadAll(r.Body) + if string(b) == string(loginPayload) { + + // login request needs to be of the right content-typ + if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { + w.WriteHeader(http.StatusBadRequest) + } + + w.Header().Set("Content-Type", "application/json") + http.SetCookie(w, &http.Cookie{Name: "QSESSIONID", Value: "94ed00f482249dd77arIcp6eBBJaik", Path: "/"}) + _, _ = w.Write(loginResponse) + } else { + w.WriteHeader(http.StatusBadRequest) + } + case "DELETE": + //1for h, values := range r.Header { + //1 for _, v := range values { + //1 fmt.Println(h, v) + //1 } + //1} + if r.Header.Get("X-Csrftoken") != "l5L29IP7" { + w.WriteHeader(http.StatusBadRequest) + } + } +} diff --git a/providers/dell/idrac8/idrac8.go b/providers/dell/idrac8/idrac8.go index c894c530..04af8dec 100644 --- a/providers/dell/idrac8/idrac8.go +++ b/providers/dell/idrac8/idrac8.go @@ -882,3 +882,18 @@ func (i *IDrac8) UpdateCredentials(username string, password string) { i.username = username i.password = password } + +// BiosVersion returns the BIOS version from the BMC, implements the Firmware interface +func (i *IDrac8) GetBIOSVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// BMCVersion returns the BMC version, implements the Firmware interface +func (i *IDrac8) GetBMCVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// Updates the BMC firmware, implements the Firmware interface +func (i *IDrac8) FirmwareUpdateBMC(ctx context.Context, filePath string) error { + return errors.ErrNotImplemented +} diff --git a/providers/dell/idrac9/idrac9.go b/providers/dell/idrac9/idrac9.go index 80999cfa..fc3fc9a5 100644 --- a/providers/dell/idrac9/idrac9.go +++ b/providers/dell/idrac9/idrac9.go @@ -925,3 +925,18 @@ func (i *IDrac9) UpdateCredentials(username string, password string) { i.username = username i.password = password } + +// BiosVersion returns the BIOS version from the BMC, implements the Firmware interface +func (i *IDrac9) GetBIOSVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// BMCVersion returns the BMC version, implements the Firmware interface +func (i *IDrac9) GetBMCVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// Updates the BMC firmware, implements the Firmware interface +func (i *IDrac9) FirmwareUpdateBMC(ctx context.Context, filePath string) error { + return errors.ErrNotImplemented +} diff --git a/providers/dummy/ibmc/ibmc.go b/providers/dummy/ibmc/ibmc.go index 2a069432..9225b033 100644 --- a/providers/dummy/ibmc/ibmc.go +++ b/providers/dummy/ibmc/ibmc.go @@ -5,6 +5,7 @@ import ( "github.com/bmc-toolbox/bmclib/cfgresources" "github.com/bmc-toolbox/bmclib/devices" + "github.com/bmc-toolbox/bmclib/errors" ) // The ibmc model is part of the dummy vendor, @@ -186,3 +187,18 @@ func (i *Ibmc) UpdateFirmware(string, string) (b bool, e error) { func (i *Ibmc) IsOn() (status bool, err error) { return false, nil } + +// BiosVersion returns the BIOS version from the BMC, implements the Firmware interface +func (i *Ibmc) GetBIOSVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// BMCVersion returns the BMC version, implements the Firmware interface +func (i *Ibmc) GetBMCVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// Updates the BMC firmware, implements the Firmware interface +func (i *Ibmc) FirmwareUpdateBMC(ctx context.Context, filePath string) error { + return errors.ErrNotImplemented +} diff --git a/providers/hp/ilo/ilo.go b/providers/hp/ilo/ilo.go index b904041f..12e53a4a 100644 --- a/providers/hp/ilo/ilo.go +++ b/providers/hp/ilo/ilo.go @@ -843,3 +843,18 @@ func (i *Ilo) UpdateCredentials(username string, password string) { i.username = username i.password = password } + +// BiosVersion returns the BIOS version from the BMC, implements the Firmware interface +func (i *Ilo) GetBIOSVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// BMCVersion returns the BMC version, implements the Firmware interface +func (i *Ilo) GetBMCVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// Updates the BMC firmware, implements the Firmware interface +func (i *Ilo) FirmwareUpdateBMC(ctx context.Context, filePath string) error { + return errors.ErrNotImplemented +} diff --git a/providers/providers.go b/providers/providers.go index a4577c83..887fa01a 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -21,4 +21,12 @@ const ( FeatureBmcReset registrar.Feature = "bmcreset" // FeatureBootDeviceSet means an implementation the next boot device FeatureBootDeviceSet registrar.Feature = "bootdeviceset" + // FeatureBmcVersionRead means an implementation that returns the BMC firmware version + FeatureBmcVersionRead registrar.Feature = "bmcversionread" + // FeatureBiosVersionRead means an implementation that returns the BIOS firmware version + FeatureBiosVersionRead registrar.Feature = "biosversionread" + // FeatureBmcFirmwareUpdate means an implementation that updates the BMC firmware + FeatureBmcFirmwareUpdate registrar.Feature = "bmcfirwareupdate" + // FeatureBiosFirmwareUpdate means an implementation that updates the BIOS firmware + FeatureBiosFirmwareUpdate registrar.Feature = "biosfirwareupdate" ) diff --git a/providers/supermicro/supermicrox/supermicrox.go b/providers/supermicro/supermicrox/supermicrox.go index 2b871274..5194cb12 100644 --- a/providers/supermicro/supermicrox/supermicrox.go +++ b/providers/supermicro/supermicrox/supermicrox.go @@ -721,3 +721,18 @@ func (s *SupermicroX) UpdateCredentials(username string, password string) { s.username = username s.password = password } + +// BiosVersion returns the BIOS version from the BMC, implements the Firmware interface +func (s *SupermicroX) GetBIOSVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// BMCVersion returns the BMC version, implements the Firmware interface +func (s *SupermicroX) GetBMCVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// Updates the BMC firmware, implements the Firmware interface +func (s *SupermicroX) FirmwareUpdateBMC(ctx context.Context, filePath string) error { + return errors.ErrNotImplemented +} diff --git a/providers/supermicro/supermicrox11/supermicrox.go b/providers/supermicro/supermicrox11/supermicrox.go index dc44674d..0ecbe543 100644 --- a/providers/supermicro/supermicrox11/supermicrox.go +++ b/providers/supermicro/supermicrox11/supermicrox.go @@ -724,3 +724,18 @@ func (s *SupermicroX) UpdateCredentials(username string, password string) { s.username = username s.password = password } + +// BiosVersion returns the BIOS version from the BMC, implements the Firmware interface +func (s *SupermicroX) GetBIOSVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// BMCVersion returns the BMC version, implements the Firmware interface +func (s *SupermicroX) GetBMCVersion(ctx context.Context) (string, error) { + return "", errors.ErrNotImplemented +} + +// Updates the BMC firmware, implements the Firmware interface +func (s *SupermicroX) FirmwareUpdateBMC(ctx context.Context, filePath string) error { + return errors.ErrNotImplemented +}