From d8e47cd1a1b036a6a810b5fd21fcd56d0a169248 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Thu, 6 Feb 2025 17:25:56 -0500 Subject: [PATCH] feat: enhance firmware update functionality and add BMC identification support --- README.md | 2 +- internal/update.go | 52 ++++++++++++++++++------------- pkg/crawler/identify.go | 69 +++++++++++++++++++++++++++++++++++++++++ pkg/crawler/main.go | 56 +++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 22 deletions(-) create mode 100644 pkg/crawler/identify.go diff --git a/README.md b/README.md index 6cb34be..424fe7f 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Note: If the `cache` flag is not set, `magellan` will use `/tmp/$USER/magellan.d ### Updating Firmware -The `magellan` tool is capable of updating firmware with using the `update` subcommand via the Redfish API. This may sometimes necessary if some of the `collect` output is missing or is not including what is expected. The subcommand expects there to be a running HTTP/HTTPS server running that has an accessible URL path to the firmware download. Specify the URL with the `--firmware-path` flag and the firmware type with the `--component` flag with all the other usual arguments like in the example below: +The `magellan` tool is capable of updating firmware with using the `update` subcommand via the Redfish API. This may sometimes necessary if some of the `collect` output is missing or is not including what is expected. The subcommand expects there to be a running HTTP/HTTPS server running that has an accessible URL path to the firmware download. Specify the URL with the `--firmware-path` flag and the firmware type with the `--component` flag (optional) with all the other usual arguments like in the example below: ```bash ./magellan update 172.16.0.108:443 \ diff --git a/internal/update.go b/internal/update.go index 9191818..2928beb 100644 --- a/internal/update.go +++ b/internal/update.go @@ -1,12 +1,13 @@ package magellan import ( - "encoding/json" "fmt" "net/http" "net/url" "github.com/OpenCHAMI/magellan/pkg/client" + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/redfish" ) type UpdateParams struct { @@ -20,38 +21,47 @@ type UpdateParams struct { // UpdateFirmwareRemote() uses 'gofish' to update the firmware of a BMC node. // The function expects the firmware URL, firmware version, and component flags to be // set from the CLI to perform a firmware update. +// Example: +// ./magellan update https://192.168.23.40 --username root --password 0penBmc +// --firmware-url http://192.168.23.19:1337/obmc-phosphor-image.static.mtd.tar +// --scheme TFTP +// +// being: +// q.URI https://192.168.23.40 +// q.TransferProtocol TFTP +// q.FirmwarePath http://192.168.23.19:1337/obmc-phosphor-image.static.mtd.tar func UpdateFirmwareRemote(q *UpdateParams) error { // parse URI to set up full address uri, err := url.ParseRequestURI(q.URI) if err != nil { return fmt.Errorf("failed to parse URI: %w", err) } - uri.User = url.UserPassword(q.Username, q.Password) - // set up other vars - updateUrl := fmt.Sprintf("%s/redfish/v1/UpdateService/Actions/SimpleUpdate", uri.String()) - 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) + // Connect to the Redfish service using gofish (using insecure connection for this example) + client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: q.Username, Password: q.Password, Insecure: true}) if err != nil { - return fmt.Errorf("failed to marshal data: %v", err) + return fmt.Errorf("failed to connect to Redfish service: %w", err) } - res, body, err := client.MakeRequest(nil, updateUrl, "POST", data, headers) + defer client.Logout() + + // Retrieve the UpdateService from the Redfish client + updateService, err := client.Service.UpdateService() if err != nil { - return fmt.Errorf("something went wrong: %v", err) - } else if res == nil { - return fmt.Errorf("no response returned (url: %s)", updateUrl) + return fmt.Errorf("failed to get update service: %w", err) } - if len(body) > 0 { - fmt.Printf("%d: %v\n", res.StatusCode, string(body)) + + // Build the update request payload + req := redfish.SimpleUpdateParameters{ + ImageURI: q.FirmwarePath, + TransferProtocol: redfish.TransferProtocolType(q.TransferProtocol), } + + // Execute the SimpleUpdate action + err = updateService.SimpleUpdate(&req) + if err != nil { + return fmt.Errorf("firmware update failed: %w", err) + } + fmt.Println("Firmware update initiated successfully.") return nil } diff --git a/pkg/crawler/identify.go b/pkg/crawler/identify.go new file mode 100644 index 0000000..be5b788 --- /dev/null +++ b/pkg/crawler/identify.go @@ -0,0 +1,69 @@ +package crawler + +import ( + "fmt" + + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/redfish" +) + +// BMCInfo represents relevant information about a BMC +type BMCInfo struct { + Manufacturer string `json:"manufacturer"` + Model string `json:"model"` + SerialNumber string `json:"serial_number"` + FirmwareVersion string `json:"firmware_version"` + ManagerType string `json:"manager_type"` + UUID string `json:"uuid"` +} + +// IsBMC checks if a given Manager is a BMC based on its type and associations +func IsBMC(manager *redfish.Manager) bool { + if manager == nil { + return false + } + + // Valid BMC types in Redfish + bmcTypes := map[string]bool{ + "BMC": true, + "ManagementController": true, // Some BMCs use this type + } + + // Check if ManagerType matches a BMC type + if !bmcTypes[string(manager.ManagerType)] { + return false + } + + return false // Otherwise, it's likely a chassis manager or other device +} + +// GetBMCInfo retrieves details of all available BMCs +func GetBMCInfo(client *gofish.APIClient) ([]BMCInfo, error) { + var bmcList []BMCInfo + + // Retrieve all managers (BMCs and other managers) + managers, err := client.Service.Managers() + if err != nil { + return nil, fmt.Errorf("failed to retrieve managers: %v", err) + } + + // Iterate through each manager and collect BMC details + for _, manager := range managers { + if !IsBMC(manager) { + continue // Skip if it's not a BMC + } + + bmc := BMCInfo{ + Manufacturer: manager.Manufacturer, + Model: manager.Model, + SerialNumber: manager.SerialNumber, + FirmwareVersion: manager.FirmwareVersion, + ManagerType: string(manager.ManagerType), // Convert ManagerType to string + UUID: manager.UUID, + } + + bmcList = append(bmcList, bmc) + } + + return bmcList, nil +} diff --git a/pkg/crawler/main.go b/pkg/crawler/main.go index 771efb9..f9526cd 100644 --- a/pkg/crawler/main.go +++ b/pkg/crawler/main.go @@ -130,6 +130,24 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) { } // CrawlBMCForSystems pulls BMC manager information. +// CrawlBMCForManagers connects to a BMC (Baseboard Management Controller) using the provided configuration, +// retrieves the ServiceRoot, and then fetches the list of managers from the ServiceRoot. +// +// Parameters: +// - config: A CrawlerConfig struct containing the URI, username, password, and other connection details. +// +// Returns: +// - []Manager: A slice of Manager structs representing the managers retrieved from the BMC. +// - error: An error object if any error occurs during the connection or retrieval process. +// +// The function performs the following steps: +// 1. Initializes a gofish client with the provided configuration. +// 2. Attempts to connect to the BMC using the gofish client. +// 3. Handles specific connection errors such as 404 (ServiceRoot not found) and 401 (authentication failed). +// 4. Logs out from the client after the operations are completed. +// 5. Retrieves the ServiceRoot from the connected BMC. +// 6. Fetches the list of managers from the ServiceRoot. +// 7. Returns the list of managers and any error encountered during the process. func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) { // initialize gofish client var managers []Manager @@ -165,6 +183,27 @@ func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) { return walkManagers(rf_managers, config.URI) } +// walkSystems processes a list of Redfish computer systems and their associated chassis, +// and returns a list of inventory details for each system. +// +// Parameters: +// - rf_systems: A slice of pointers to redfish.ComputerSystem objects representing the computer systems to be processed. +// - rf_chassis: A pointer to a redfish.Chassis object representing the chassis associated with the computer systems. +// - baseURI: A string representing the base URI for constructing resource URIs. +// +// Returns: +// - A slice of InventoryDetail objects containing detailed information about each computer system. +// - An error if any issues occur while processing the computer systems or their associated resources. +// +// The function performs the following steps: +// 1. Iterates over each computer system in rf_systems. +// 2. Constructs an InventoryDetail object for each computer system, populating fields such as URI, UUID, Name, Manufacturer, SystemType, Model, Serial, BiosVersion, PowerState, ProcessorCount, ProcessorType, and MemoryTotal. +// 3. If rf_chassis is not nil, populates additional chassis-related fields in the InventoryDetail object. +// 4. Retrieves and processes Ethernet interfaces for each computer system, adding them to the EthernetInterfaces field of the InventoryDetail object. +// 5. Retrieves and processes Network interfaces and their associated network adapters for each computer system, adding them to the NetworkInterfaces field of the InventoryDetail object. +// 6. Processes trusted modules for each computer system, adding them to the TrustedModules field of the InventoryDetail object. +// 7. Appends the populated InventoryDetail object to the systems slice. +// 8. Returns the systems slice and any error encountered during processing. func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chassis, baseURI string) ([]InventoryDetail, error) { systems := []InventoryDetail{} for _, rf_computersystem := range rf_systems { @@ -253,6 +292,23 @@ func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chass return systems, nil } +// walkManagers processes a list of Redfish managers and extracts relevant information +// to create a slice of Manager objects. +// +// Parameters: +// +// rf_managers - A slice of pointers to redfish.Manager objects representing the Redfish managers to be processed. +// baseURI - A string representing the base URI to be used for constructing URIs for the managers and their Ethernet interfaces. +// +// Returns: +// +// A slice of Manager objects containing the extracted information from the provided Redfish managers. +// An error if any issues occur while retrieving Ethernet interfaces from the managers. +// +// The function iterates over each Redfish manager, retrieves its Ethernet interfaces, +// and constructs a Manager object with the relevant details, including Ethernet interface information. +// If an error occurs while retrieving Ethernet interfaces, the function logs the error and returns the managers +// collected so far along with the error. func walkManagers(rf_managers []*redfish.Manager, baseURI string) ([]Manager, error) { var managers []Manager for _, rf_manager := range rf_managers {