Merge pull request #10 from davidallendj/update-firmware

Added `update` command to update firmware
This commit is contained in:
David Allen 2023-10-16 10:35:37 -06:00 committed by GitHub
commit 075cbf0733
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 403 additions and 75 deletions

View file

@ -1,7 +1,8 @@
# Magellan # Magellan
Magellan is a small tool designed to scan a network and collect BMC information Magellan is a board management controller discovery tool designed to scan a network
to load the data into an [`hms-smd`](https://github.com/alexlovelltroy/hms-smd/tree/master) instance. and collect information about a BMC node and load that data into an
[`hms-smd`](https://github.com/bikeshack/smd/tree/master) instance.
## How It Works ## How It Works
@ -13,9 +14,13 @@ Magellan is designed to do three things:
Magellan first tries to probe for specified hosts using the [`dora`](https://github.com/bmc-toolbox/dora) Magellan first tries to probe for specified hosts using the [`dora`](https://github.com/bmc-toolbox/dora)
API. If that fails, it then tries to use its own built-in, simpler scanner as a fallback. API. If that fails, it then tries to use its own built-in, simpler scanner as a fallback.
Next, it tries to query information about the BMC node using `bmclib` functions, but requires This is done by sending a raw TCP request to a number of potential hosts over a
access to a redfish interface on the node to work. Once the BMC information is received, network, and noting which requests are successful. At this point, `magellan` sees
it is then stored into `hms-smd` using its API. no difference between a services.
Next, it tries to query information about the BMC node using `bmclib` functions,
but requires access to a redfish interface on the node to work. Once the BMC
information is received, it is then stored into `hms-smd` using its API.
In summary, `magellan` needs at minimum the following configured to work on each node: In summary, `magellan` needs at minimum the following configured to work on each node:
@ -33,7 +38,7 @@ cd magellan
go mod tidy && go build go mod tidy && go build
``` ```
This should find and download all of the required dependencies. Although other This should find and download all of the required dependencies. Although other
versions of Go may work, the project has only been tested with v1.20. versions of Go may work, the project has only been tested with v1.20.
## Usage ## Usage

View file

@ -1,17 +1,21 @@
function build(){ function build(){
go mod tidy && go build -C bin/magellan go mod tidy && go build -C bin/magellan
} }
function scan() { function scan() {
./magellan scan --subnet 172.16.0.0 --port 443 ./magellan scan --subnet 172.16.0.0 --port 443
} }
function list(){ function list() {
./magellan list ./magellan list
}
function update() {
./magellan update --user admin --pass password --host 172.16.0.109 --component BMC --protocol HTTP --firmware-path ""
} }
function collect() { function collect() {
./magellan collect --user admin --pass password ./magellan collect --user admin --pass password
} }

View file

@ -33,6 +33,7 @@ var collectCmd = &cobra.Command{
q := &magellan.QueryParams{ q := &magellan.QueryParams{
User: user, User: user,
Pass: pass, Pass: pass,
Protocol: protocol,
Drivers: drivers, Drivers: drivers,
Preferred: preferredDriver, Preferred: preferredDriver,
Timeout: timeout, Timeout: timeout,
@ -58,6 +59,7 @@ func init() {
collectCmd.PersistentFlags().IntVarP(&smd.Port, "port", "p", smd.Port, "set the port to the smd API") collectCmd.PersistentFlags().IntVarP(&smd.Port, "port", "p", smd.Port, "set the port to the smd API")
collectCmd.PersistentFlags().StringVar(&user, "user", "", "set the BMC user") collectCmd.PersistentFlags().StringVar(&user, "user", "", "set the BMC user")
collectCmd.PersistentFlags().StringVar(&pass, "pass", "", "set the BMC password") collectCmd.PersistentFlags().StringVar(&pass, "pass", "", "set the BMC password")
collectCmd.PersistentFlags().StringVar(&protocol, "protocol", "https", "set the Redfish protocol")
collectCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "/tmp/magellan/data/", "set the path to store collection data") collectCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "/tmp/magellan/data/", "set the path to store collection data")
collectCmd.PersistentFlags().BoolVar(&forceUpdate, "force-update", true, "set flag to force update data sent to SMD ") collectCmd.PersistentFlags().BoolVar(&forceUpdate, "force-update", true, "set flag to force update data sent to SMD ")
collectCmd.PersistentFlags().StringVar(&preferredDriver, "preferred-driver", "ipmi", "set the preferred driver to use") collectCmd.PersistentFlags().StringVar(&preferredDriver, "preferred-driver", "ipmi", "set the preferred driver to use")

View file

@ -12,6 +12,7 @@ var (
threads int threads int
ports []int ports []int
hosts []string hosts []string
protocol string
withSecureTLS bool withSecureTLS bool
certPoolFile string certPoolFile string
user string user string

82
cmd/update.go Normal file
View file

@ -0,0 +1,82 @@
package cmd
import (
magellan "github.com/bikeshack/magellan/internal"
"github.com/bikeshack/magellan/internal/log"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
host string
port int
firmwarePath string
firmwareVersion string
component string
transferProtocol string
status bool
)
var updateCmd = &cobra.Command{
Use: "update",
Short: "Update BMC node firmware",
Run: func(cmd *cobra.Command, args []string) {
l := log.NewLogger(logrus.New(), logrus.DebugLevel)
q := &magellan.UpdateParams {
FirmwarePath: firmwarePath,
FirmwareVersion: firmwareVersion,
Component: component,
TransferProtocol: transferProtocol,
QueryParams: magellan.QueryParams{
Drivers: []string{"redfish"},
Preferred: "redfish",
Protocol: protocol,
Host: host,
User: user,
Pass: pass,
Timeout: timeout,
Port: port,
WithSecureTLS: withSecureTLS,
},
}
// check if required params are set
if host == "" || user == "" || pass == "" {
l.Log.Fatal("requires host, user, and pass to be set")
}
// get status if flag is set and exit
if status {
err := magellan.GetUpdateStatus(q)
if err != nil {
l.Log.Errorf("could not get update status: %v", err)
}
return
}
// client, err := magellan.NewClient(l, &q.QueryParams)
// if err != nil {
// l.Log.Errorf("could not make client: %v", err)
// }
// err = magellan.UpdateFirmware(client, l, q)
err := magellan.UpdateFirmwareRemote(q)
if err != nil {
l.Log.Errorf("could not update firmware: %v", err)
}
},
}
func init() {
updateCmd.Flags().StringVar(&host, "host", "", "set the BMC host")
updateCmd.Flags().IntVar(&port, "port", 443, "set the BMC port")
updateCmd.Flags().StringVar(&user, "user", "", "set the BMC user")
updateCmd.Flags().StringVar(&pass, "pass", "", "set the BMC password")
updateCmd.Flags().StringVar(&transferProtocol, "transfer-protocol", "HTTP", "set the transfer protocol")
updateCmd.Flags().StringVar(&protocol, "protocol", "https", "set the Redfish protocol")
updateCmd.Flags().StringVar(&firmwarePath, "firmware-path", "", "set the path to the firmware")
updateCmd.Flags().StringVar(&firmwareVersion, "firmware-version", "", "set the version of firmware to be installed")
updateCmd.Flags().StringVar(&component, "component", "", "set the component to upgrade")
updateCmd.Flags().BoolVar(&withSecureTLS, "secure-tls", false, "enable secure TLS")
updateCmd.Flags().BoolVar(&status, "status", false, "get the status of the update")
rootCmd.AddCommand(updateCmd)
}

View file

@ -51,9 +51,7 @@ func AddRedfishEndpoint(data []byte, headers map[string]string) error {
// Add redfish endpoint via POST `/hsm/v2/Inventory/RedfishEndpoints` endpoint // Add redfish endpoint via POST `/hsm/v2/Inventory/RedfishEndpoints` endpoint
url := makeEndpointUrl("/Inventory/RedfishEndpoints") url := makeEndpointUrl("/Inventory/RedfishEndpoints")
res, body, err := util.MakeRequest(url, "POST", data, headers) res, body, err := util.MakeRequest(url, "POST", data, headers)
fmt.Printf("smd url: %v\n", url) fmt.Printf("%v (%v)\n%s\n", url, res.Status, string(body))
fmt.Printf("res: %v\n", res.Status)
fmt.Printf("body: %v\n", string(body))
if res != nil { if res != nil {
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
return fmt.Errorf("could not add redfish endpoint") return fmt.Errorf("could not add redfish endpoint")
@ -69,9 +67,7 @@ func UpdateRedfishEndpoint(xname string, data []byte, headers map[string]string)
// Update redfish endpoint via PUT `/hsm/v2/Inventory/RedfishEndpoints` endpoint // Update redfish endpoint via PUT `/hsm/v2/Inventory/RedfishEndpoints` endpoint
url := makeEndpointUrl("/Inventory/RedfishEndpoints/" + xname) url := makeEndpointUrl("/Inventory/RedfishEndpoints/" + xname)
res, body, err := util.MakeRequest(url, "PUT", data, headers) res, body, err := util.MakeRequest(url, "PUT", data, headers)
fmt.Printf("smd url: %v\n", url) fmt.Printf("%v (%v)\n%s\n", url, res.Status, string(body))
fmt.Printf("res: %v\n", res.Status)
fmt.Printf("body: %v\n", string(body))
if res != nil { if res != nil {
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
return fmt.Errorf("could not update redfish endpoint") return fmt.Errorf("could not update redfish endpoint")

View file

@ -38,6 +38,7 @@ const (
type QueryParams struct { type QueryParams struct {
Host string Host string
Port int Port int
Protocol string
User string User string
Pass string Pass string
Drivers []string Drivers []string
@ -53,9 +54,6 @@ type QueryParams struct {
} }
func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) { func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) {
// NOTE: bmclib.NewClient(host, port, user, pass)
// ...seems like the `port` params doesn't work like expected depending on interface
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
} }
@ -77,22 +75,20 @@ func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) {
} }
// only work if valid cert is provided // only work if valid cert is provided
if q.WithSecureTLS { if q.WithSecureTLS && q.CertPoolFile != "" {
var pool *x509.CertPool pool := x509.NewCertPool()
if q.CertPoolFile != "" { data, err := os.ReadFile(q.CertPoolFile)
pool = x509.NewCertPool() if err != nil {
data, err := os.ReadFile(q.CertPoolFile) return nil, fmt.Errorf("could not read cert pool file: %v", err)
if err != nil {
return nil, fmt.Errorf("could not read cert pool file: %v", err)
}
pool.AppendCertsFromPEM(data)
} }
pool.AppendCertsFromPEM(data)
// a nil pool uses the system certs // a nil pool uses the system certs
clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool))
} }
url := "" url := ""
fmt.Println(url)
if q.User != "" && q.Pass != "" { if q.User != "" && q.Pass != "" {
url += fmt.Sprintf("https://%s:%s@%s", q.User, q.Pass, q.Host) url += fmt.Sprintf("%s://%s:%s@%s", q.Protocol, q.User, q.Pass, q.Host)
} else { } else {
url += q.Host url += q.Host
} }
@ -172,7 +168,7 @@ func CollectInfo(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) er
var rm map[string]json.RawMessage var rm map[string]json.RawMessage
// inventories // inventories
inventory, err := QueryInventory(client, l, q) inventory, err := QueryInventory(client, q)
if err != nil { if err != nil {
l.Log.Errorf("could not query inventory (%v:%v): %v", q.Host, q.Port, err) l.Log.Errorf("could not query inventory (%v:%v): %v", q.Host, q.Port, err)
} }
@ -189,7 +185,7 @@ func CollectInfo(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) er
data["Chassis"] = rm["Chassis"] data["Chassis"] = rm["Chassis"]
// ethernet interfaces // ethernet interfaces
interfaces, err := QueryEthernetInterfaces(client, l, q) interfaces, err := QueryEthernetInterfaces(client, q)
if err != nil { if err != nil {
l.Log.Errorf("could not query ethernet interfaces: %v", err) l.Log.Errorf("could not query ethernet interfaces: %v", err)
continue continue
@ -312,7 +308,7 @@ func CollectInfo(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) er
return nil return nil
} }
func QueryMetadata(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryMetadata(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// client, err := NewClient(l, q) // client, err := NewClient(l, q)
// open BMC session and update driver registry // open BMC session and update driver registry
@ -346,7 +342,7 @@ func QueryMetadata(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte
return b, nil return b, nil
} }
func QueryInventory(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryInventory(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// open BMC session and update driver registry // open BMC session and update driver registry
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout)) ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx) client.Registry.FilterForCompatible(ctx)
@ -379,7 +375,7 @@ func QueryInventory(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byt
return b, nil return b, nil
} }
func QueryPowerState(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryPowerState(client *bmclib.Client, q *QueryParams) ([]byte, error) {
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout)) ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx) client.Registry.FilterForCompatible(ctx)
err := client.PreferProvider(q.Preferred).Open(ctx) err := client.PreferProvider(q.Preferred).Open(ctx)
@ -411,7 +407,7 @@ func QueryPowerState(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]by
} }
func QueryUsers(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryUsers(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// open BMC session and update driver registry // open BMC session and update driver registry
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout)) ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx) client.Registry.FilterForCompatible(ctx)
@ -445,7 +441,7 @@ func QueryUsers(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, e
return b, nil return b, nil
} }
func QueryBios(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryBios(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// client, err := NewClient(l, q) // client, err := NewClient(l, q)
// if err != nil { // if err != nil {
// return nil, fmt.Errorf("could not make query: %v", err) // return nil, fmt.Errorf("could not make query: %v", err)
@ -457,14 +453,27 @@ func QueryBios(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, er
return b, err return b, err
} }
func QueryEthernetInterfaces(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryEthernetInterfaces(client *bmclib.Client, q *QueryParams) ([]byte, error) {
c, err := connectGofish(q) c, err := connectGofish(q)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not connect to bmc: %v", err) return nil, fmt.Errorf("could not connect to bmc: %v", err)
} }
interfaces, err := redfish.ListReferencedEthernetInterfaces(c, "/redfish/v1/Systems/") systems, err := c.Service.Systems()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not query storage systems (%v:%v): %v", q.Host, q.Port, err)
}
var interfaces []*redfish.EthernetInterface
for _, system := range systems {
i, err := redfish.ListReferencedEthernetInterfaces(c, "/redfish/v1/Systems/" + system.ID + "/EthernetInterfaces/")
if err != nil {
continue
}
interfaces = append(interfaces, i...)
}
if len(interfaces) <= 0 {
return nil, fmt.Errorf("could not get ethernet interfaces: %v", err) return nil, fmt.Errorf("could not get ethernet interfaces: %v", err)
} }
@ -582,12 +591,7 @@ func QueryRegisteries(q *QueryParams) ([]byte, error) {
} }
func QueryProcessors(q *QueryParams) ([]byte, error) { func QueryProcessors(q *QueryParams) ([]byte, error) {
baseUrl := "https://" url := baseRedfishUrl(q) + "/Systems"
if q.User != "" && q.Pass != "" {
baseUrl += fmt.Sprintf("%s:%s@", q.User, q.Pass)
}
baseUrl += fmt.Sprintf("%s:%d", q.Host, q.Port)
url := baseUrl + "/redfish/v1/Systems"
res, body, err := util.MakeRequest(url, "GET", nil, nil) res, body, err := util.MakeRequest(url, "GET", nil, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("something went wrong: %v", err) return nil, fmt.Errorf("something went wrong: %v", err)
@ -606,7 +610,7 @@ func QueryProcessors(q *QueryParams) ([]byte, error) {
// request data about each processor member on node // request data about each processor member on node
for _, member := range members { for _, member := range members {
var oid = member["@odata.id"].(string) var oid = member["@odata.id"].(string)
var infoUrl = baseUrl + oid var infoUrl = url + oid
res, _, err := util.MakeRequest(infoUrl, "GET", nil, nil) res, _, err := util.MakeRequest(infoUrl, "GET", nil, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("something went wrong: %v", err) return nil, fmt.Errorf("something went wrong: %v", err)
@ -633,6 +637,7 @@ func connectGofish(q *QueryParams) (*gofish.APIClient, error) {
config := makeGofishConfig(q) config := makeGofishConfig(q)
c, err := gofish.Connect(config) c, err := gofish.Connect(config)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not connect to redfish endpoint: %v", err) return nil, fmt.Errorf("could not connect to redfish endpoint: %v", err)
} }
if c != nil { if c != nil {
@ -647,12 +652,7 @@ func connectGofish(q *QueryParams) (*gofish.APIClient, error) {
} }
func makeGofishConfig(q *QueryParams) gofish.ClientConfig { func makeGofishConfig(q *QueryParams) gofish.ClientConfig {
url := "https://" url := baseRedfishUrl(q)
if q.User != "" && q.Pass != "" {
url += fmt.Sprintf("%s:%s@", q.User, q.Pass)
}
url += fmt.Sprintf("%s:%d", q.Host, q.Port)
return gofish.ClientConfig{ return gofish.ClientConfig{
Endpoint: url, Endpoint: url,
Username: q.User, Username: q.User,
@ -690,3 +690,11 @@ func makeJson(object any) ([]byte, error) {
} }
return []byte(b), nil return []byte(b), nil
} }
func baseRedfishUrl(q *QueryParams) string {
url := fmt.Sprintf("%s://", q.Protocol)
if q.User != "" && q.Pass != "" {
url += fmt.Sprintf("%s:%s@", q.User, q.Pass)
}
return fmt.Sprintf("%s%s:%d", url, q.Host, q.Port)
}

View file

@ -8,12 +8,7 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
func InsertProbeResults(path string, states *[]magellan.ScannedResult) error { func CreateProbeResultsIfNotExists(path string) (*sqlx.DB, error) {
if states == nil {
return fmt.Errorf("states == nil")
}
// create database if it doesn't already exist
schema := ` schema := `
CREATE TABLE IF NOT EXISTS magellan_scanned_ports ( CREATE TABLE IF NOT EXISTS magellan_scanned_ports (
host TEXT NOT NULL, host TEXT NOT NULL,
@ -25,9 +20,22 @@ func InsertProbeResults(path string, states *[]magellan.ScannedResult) error {
` `
db, err := sqlx.Open("sqlite3", path) db, err := sqlx.Open("sqlite3", path)
if err != nil { if err != nil {
return fmt.Errorf("could not open database: %v", err) return nil, fmt.Errorf("could not open database: %v", err)
} }
db.MustExec(schema) db.MustExec(schema)
return db, nil
}
func InsertProbeResults(path string, states *[]magellan.BMCProbeResult) error {
if states == nil {
return fmt.Errorf("states == nil")
}
// create database if it doesn't already exist
db, err := CreateProbeResultsIfNotExists(path)
if err != nil {
return err
}
// insert all probe states into db // insert all probe states into db
tx := db.MustBegin() tx := db.MustBegin()
@ -46,6 +54,30 @@ func InsertProbeResults(path string, states *[]magellan.ScannedResult) error {
return nil return nil
} }
func DeleteProbeResults(path string, results *[]magellan.BMCProbeResult) error {
if results == nil {
return fmt.Errorf("no probe results found")
}
db, err := sqlx.Open("sqlite3", path)
if err != nil {
return fmt.Errorf("could not open database: %v", err)
}
tx := db.MustBegin()
for _, state := range *results {
sql := `DELETE FROM magellan_scanned_ports WHERE host = :host, port = :port;`
_, err := tx.NamedExec(sql, &state)
if err != nil {
fmt.Printf("could not execute transaction: %v\n", err)
}
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("could not commit transaction: %v", err)
}
return nil
}
func GetProbeResults(path string) ([]magellan.ScannedResult, error) { func GetProbeResults(path string) ([]magellan.ScannedResult, error) {
db, err := sqlx.Open("sqlite3", path) db, err := sqlx.Open("sqlite3", path)
if err != nil { if err != nil {

195
internal/update.go Normal file
View file

@ -0,0 +1,195 @@
package magellan
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/bikeshack/magellan/internal/log"
"github.com/bikeshack/magellan/internal/util"
bmclib "github.com/bmc-toolbox/bmclib/v2"
"github.com/bmc-toolbox/bmclib/v2/constants"
bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors"
"github.com/sirupsen/logrus"
)
type UpdateParams struct {
QueryParams
FirmwarePath string
FirmwareVersion string
Component string
TransferProtocol string
}
// NOTE: Does not work since OpenBMC, whic bmclib uses underneath, does not
// support multipart updates. See issue: https://github.com/bmc-toolbox/bmclib/issues/341
func UpdateFirmware(client *bmclib.Client, l *log.Logger, q *UpdateParams) error {
if q.Component == "" {
return fmt.Errorf("component is required")
}
// open BMC session and update driver registry
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx)
err := client.Open(ctx)
if err != nil {
ctxCancel()
return fmt.Errorf("could not connect to bmc: %v", err)
}
defer client.Close(ctx)
file, err := os.Open(q.FirmwarePath)
if err != nil {
ctxCancel()
return fmt.Errorf("could not open firmware path: %v", err)
}
defer file.Close()
taskId, err := client.FirmwareInstall(ctx, q.Component, constants.FirmwareApplyOnReset, true, file)
if err != nil {
ctxCancel()
return fmt.Errorf("could not install firmware: %v", err)
}
for {
if ctx.Err() != nil {
ctxCancel()
return fmt.Errorf("context error: %v", ctx.Err())
}
state, err := client.FirmwareInstallStatus(ctx, q.FirmwareVersion, q.Component, taskId)
if err != nil {
// when its under update a connection refused is returned
if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "operation timed out") {
l.Log.Info("BMC refused connection, BMC most likely resetting...")
time.Sleep(2 * time.Second)
continue
}
if errors.Is(err, bmclibErrs.ErrSessionExpired) || strings.Contains(err.Error(), "session expired") {
err := client.Open(ctx)
if err != nil {
l.Log.Fatal(err, "bmc re-login failed")
}
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("BMC session expired, logging in...")
continue
}
l.Log.Fatal(err)
}
switch state {
case constants.FirmwareInstallRunning, constants.FirmwareInstallInitializing:
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("firmware install running")
case constants.FirmwareInstallFailed:
ctxCancel()
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("firmware install failed")
return fmt.Errorf("failed to install firmware")
case constants.FirmwareInstallComplete:
ctxCancel()
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("firmware install completed")
return nil
case constants.FirmwareInstallPowerCyleHost:
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("host powercycle required")
if _, err := client.SetPowerState(ctx, "cycle"); err != nil {
ctxCancel()
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("error power cycling host for install")
return fmt.Errorf("failed to install firmware")
}
ctxCancel()
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("host power cycled, all done!")
return nil
default:
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("unknown state returned")
}
time.Sleep(2 * time.Second)
}
return nil
}
func UpdateFirmwareRemote(q *UpdateParams) error {
url := baseRedfishUrl(&q.QueryParams) + "/redfish/v1/UpdateService/Actions/SimpleUpdate"
headers := map[string]string {
"Content-Type": "application/json",
"cache-control": "no-cache",
}
b := map[string]any{
"UpdateComponent": q.Component, // BMC, BIOS
"TransferProtocol": q.TransferProtocol,
"ImageURI": q.FirmwarePath,
}
data, err := json.Marshal(b)
if err != nil {
return fmt.Errorf("could not marshal data: %v", err)
}
res, body, err := util.MakeRequest(url, "POST", data, headers)
if err != nil {
return fmt.Errorf("something went wrong: %v", err)
} else if res == nil {
return fmt.Errorf("no response returned (url: %s)", url)
}
if len(body) > 0 {
fmt.Printf("%d: %v\n", res.StatusCode, string(body))
}
return nil
}
func GetUpdateStatus(q *UpdateParams) error {
url := baseRedfishUrl(&q.QueryParams) + "/redfish/v1/UpdateService"
res, body, err := util.MakeRequest(url, "GET", nil, nil)
if err != nil {
return fmt.Errorf("something went wrong: %v", err)
} else if res == nil {
return fmt.Errorf("no response returned (url: %s)", url)
} else if res.StatusCode != http.StatusOK {
return fmt.Errorf("returned status code %d", res.StatusCode)
}
if len(body) > 0 {
fmt.Printf("%d: %v\n", res.StatusCode, string(body))
}
return nil
}
// func UpdateFirmwareLocal(q *UpdateParams) error {
// fwUrl := baseUrl(&q.QueryParams) + ""
// url := baseUrl(&q.QueryParams) + "UpdateService/Actions/"
// headers := map[string]string {
// }
// // get etag from FW inventory
// response, err := util.MakeRequest()
// // load file from disk
// file, err := os.ReadFile(q.FirmwarePath)
// if err != nil {
// return fmt.Errorf("could not read file: %v", err)
// }
// switch q.TransferProtocol {
// case "HTTP":
// default:
// return fmt.Errorf("transfer protocol not supported")
// }
// return nil
// }

View file

@ -11,15 +11,18 @@ import (
) )
func PathExists(path string) (bool, error) { func PathExists(path string) (bool, error) {
_, err := os.Stat(path) _, err := os.Stat(path)
if err == nil { return true, nil } if err == nil { return true, nil }
if os.IsNotExist(err) { return false, nil } if os.IsNotExist(err) { return false, nil }
return false, err return false, err
} }
func MakeRequest(url string, httpMethod string, body []byte, headers map[string]string) (*http.Response, []byte, error) { func MakeRequest(url string, httpMethod string, body []byte, headers map[string]string) (*http.Response, []byte, error) {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
req, _ := http.NewRequest(httpMethod, url, bytes.NewBuffer(body)) req, err := http.NewRequest(httpMethod, url, bytes.NewBuffer(body))
if err != nil {
return nil, nil, fmt.Errorf("could not create new HTTP request: %v", err)
}
req.Header.Add("User-Agent", "magellan") req.Header.Add("User-Agent", "magellan")
for k, v := range headers { for k, v := range headers {
req.Header.Add(k, v) req.Header.Add(k, v)
@ -37,20 +40,20 @@ func MakeRequest(url string, httpMethod string, body []byte, headers map[string]
} }
func MakeOutputDirectory(path string) (string, error) { func MakeOutputDirectory(path string) (string, error) {
// get the current data + time using Go's stupid formatting // get the current data + time using Go's stupid formatting
t := time.Now() t := time.Now()
dirname := t.Format("2006-01-01 15:04:05") dirname := t.Format("2006-01-01 15:04:05")
final := path + "/" + dirname final := path + "/" + dirname
// check if path is valid and directory // check if path is valid and directory
pathExists, err := PathExists(final); pathExists, err := PathExists(final);
if err != nil { if err != nil {
return final, fmt.Errorf("could not check for existing path: %v", err) return final, fmt.Errorf("could not check for existing path: %v", err)
} }
if pathExists { if pathExists {
// make sure it is directory with 0o644 permissions // make sure it is directory with 0o644 permissions
return final, fmt.Errorf("found existing path: %v", final) return final, fmt.Errorf("found existing path: %v", final)
} }
// create directory with data + time // create directory with data + time
err = os.MkdirAll(final, 0766) err = os.MkdirAll(final, 0766)