From d8e47cd1a1b036a6a810b5fd21fcd56d0a169248 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Thu, 6 Feb 2025 17:25:56 -0500 Subject: [PATCH 01/14] 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 { From 7bdad54ed5063f641c944bea93b83b04bac67836 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Fri, 7 Feb 2025 09:52:21 -0500 Subject: [PATCH 02/14] refactor: improve Redfish service connection handling and update status retrieval --- internal/update.go | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/internal/update.go b/internal/update.go index 2928beb..183980b 100644 --- a/internal/update.go +++ b/internal/update.go @@ -2,10 +2,8 @@ package magellan import ( "fmt" - "net/http" "net/url" - "github.com/OpenCHAMI/magellan/pkg/client" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/redfish" ) @@ -37,7 +35,7 @@ func UpdateFirmwareRemote(q *UpdateParams) error { return fmt.Errorf("failed to parse URI: %w", err) } - // Connect to the Redfish service using gofish (using insecure connection for this example) + // Connect to the Redfish service using gofish (using insecure connection for this) 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 connect to Redfish service: %w", err) @@ -71,18 +69,23 @@ func GetUpdateStatus(q *UpdateParams) error { if err != nil { return fmt.Errorf("failed to parse URI: %w", err) } - uri.User = url.UserPassword(q.Username, q.Password) - updateUrl := fmt.Sprintf("%s/redfish/v1/UpdateService", uri.String()) - res, body, err := client.MakeRequest(nil, updateUrl, "GET", nil, nil) + + // Connect to the Redfish service using gofish (using insecure connection for this) + client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: q.Username, Password: q.Password, Insecure: true}) if err != nil { - return fmt.Errorf("something went wrong: %v", err) - } else if res == nil { - return fmt.Errorf("no response returned (url: %s)", updateUrl) - } else if res.StatusCode != http.StatusOK { - return fmt.Errorf("returned status code %d", res.StatusCode) + return fmt.Errorf("failed to connect to Redfish service: %w", err) } - if len(body) > 0 { - fmt.Printf("%v\n", string(body)) + defer client.Logout() + + // Retrieve the UpdateService from the Redfish client + updateService, err := client.Service.UpdateService() + if err != nil { + return fmt.Errorf("failed to get update service: %w", err) } + + // Get the update status + status := updateService.Status + fmt.Printf("Update Status: %v\n", status) + return nil } From 01f811dc077ddb897b3ed2a20b5dcfe7948e6abe Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Fri, 7 Feb 2025 09:56:36 -0500 Subject: [PATCH 03/14] feat: add --insecure flag to allow insecure connections for firmware updates --- cmd/update.go | 9 +++++++-- internal/update.go | 9 +++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cmd/update.go b/cmd/update.go index d5f9a50..7f7d3b3 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -17,6 +17,7 @@ var ( component string transferProtocol string showStatus bool + Insecure bool ) // The `update` command provides an interface to easily update firmware @@ -27,8 +28,8 @@ var updateCmd = &cobra.Command{ Short: "Update BMC node firmware", Long: "Perform an firmware update using Redfish by providing a remote firmware URL and component.\n\n" + "Examples:\n" + - " magellan update 172.16.0.108:443 --username bmc_username --password bmc_password --firmware-url http://172.16.0.200:8005/firmware/bios/image.RBU --component BIOS\n" + - " magellan update 172.16.0.108:443 --status --username bmc_username --password bmc_password", + " magellan update 172.16.0.108:443 --insecure --username bmc_username --password bmc_password --firmware-url http://172.16.0.200:8005/firmware/bios/image.RBU --component BIOS\n" + + " magellan update 172.16.0.108:443 --insecure --status --username bmc_username --password bmc_password", Run: func(cmd *cobra.Command, args []string) { // check that we have at least one host if len(args) <= 0 { @@ -44,6 +45,7 @@ var updateCmd = &cobra.Command{ FirmwareVersion: firmwareVersion, Component: component, TransferProtocol: transferProtocol, + Insecure: Insecure, CollectParams: magellan.CollectParams{ URI: arg, Username: username, @@ -63,6 +65,7 @@ var updateCmd = &cobra.Command{ FirmwareVersion: firmwareVersion, Component: component, TransferProtocol: strings.ToUpper(transferProtocol), + Insecure: Insecure, CollectParams: magellan.CollectParams{ URI: host, Username: username, @@ -85,6 +88,7 @@ func init() { updateCmd.Flags().StringVar(&firmwareVersion, "firmware-version", "", "Set the version of firmware to be installed") updateCmd.Flags().StringVar(&component, "component", "", "Set the component to upgrade (BMC|BIOS)") updateCmd.Flags().BoolVar(&showStatus, "status", false, "Get the status of the update") + updateCmd.Flags().BoolVar(&Insecure, "insecure", false, "Allow insecure connections to the server") checkBindFlagError(viper.BindPFlag("update.username", updateCmd.Flags().Lookup("username"))) checkBindFlagError(viper.BindPFlag("update.password", updateCmd.Flags().Lookup("password"))) @@ -93,6 +97,7 @@ func init() { checkBindFlagError(viper.BindPFlag("update.firmware-version", updateCmd.Flags().Lookup("firmware-version"))) checkBindFlagError(viper.BindPFlag("update.component", updateCmd.Flags().Lookup("component"))) checkBindFlagError(viper.BindPFlag("update.status", updateCmd.Flags().Lookup("status"))) + checkBindFlagError(viper.BindPFlag("update.insecure", updateCmd.Flags().Lookup("insecure"))) rootCmd.AddCommand(updateCmd) } diff --git a/internal/update.go b/internal/update.go index 183980b..ccbed6b 100644 --- a/internal/update.go +++ b/internal/update.go @@ -14,6 +14,7 @@ type UpdateParams struct { FirmwareVersion string Component string TransferProtocol string + Insecure bool } // UpdateFirmwareRemote() uses 'gofish' to update the firmware of a BMC node. @@ -35,8 +36,8 @@ func UpdateFirmwareRemote(q *UpdateParams) error { return fmt.Errorf("failed to parse URI: %w", err) } - // Connect to the Redfish service using gofish (using insecure connection for this) - client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: q.Username, Password: q.Password, Insecure: true}) + // Connect to the Redfish service using gofish + client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: q.Username, Password: q.Password, Insecure: q.Insecure}) if err != nil { return fmt.Errorf("failed to connect to Redfish service: %w", err) } @@ -70,8 +71,8 @@ func GetUpdateStatus(q *UpdateParams) error { return fmt.Errorf("failed to parse URI: %w", err) } - // Connect to the Redfish service using gofish (using insecure connection for this) - client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: q.Username, Password: q.Password, Insecure: true}) + // Connect to the Redfish service using gofish + client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: q.Username, Password: q.Password, Insecure: q.Insecure}) if err != nil { return fmt.Errorf("failed to connect to Redfish service: %w", err) } From b77f9a8c9cbb496a5ba9d5d2e3043391b4539f29 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Fri, 7 Feb 2025 11:10:10 -0500 Subject: [PATCH 04/14] chore: update golang.org/x/crypto and golang.org/x/sys dependencies to latest versions --- go.mod | 6 +++--- go.sum | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index e153625..21ee7de 100644 --- a/go.mod +++ b/go.mod @@ -49,9 +49,9 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index befbed6..9c85790 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -151,8 +151,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -164,8 +164,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From ebcd3ff917ec4fef98b6eb33c8c07ea4eb2f7ba8 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 20 Feb 2025 17:09:21 -0700 Subject: [PATCH 05/14] refactor: moved internal functions to pkg and updated refs --- cmd/collect.go | 2 +- cmd/scan.go | 2 +- cmd/update.go | 2 +- internal/cache/sqlite/sqlite.go | 2 +- {internal => pkg}/collect.go | 0 {internal => pkg}/scan.go | 0 {internal => pkg}/update.go | 0 tests/api_test.go | 2 +- 8 files changed, 5 insertions(+), 5 deletions(-) rename {internal => pkg}/collect.go (100%) rename {internal => pkg}/scan.go (100%) rename {internal => pkg}/update.go (100%) diff --git a/cmd/collect.go b/cmd/collect.go index a095b13..3e43499 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -4,9 +4,9 @@ import ( "fmt" "os/user" - magellan "github.com/OpenCHAMI/magellan/internal" "github.com/OpenCHAMI/magellan/internal/cache/sqlite" urlx "github.com/OpenCHAMI/magellan/internal/url" + magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/OpenCHAMI/magellan/pkg/auth" "github.com/cznic/mathutil" "github.com/rs/zerolog/log" diff --git a/cmd/scan.go b/cmd/scan.go index 864449f..76955c0 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -7,8 +7,8 @@ import ( "os" "path" - magellan "github.com/OpenCHAMI/magellan/internal" "github.com/OpenCHAMI/magellan/internal/cache/sqlite" + magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/rs/zerolog/log" urlx "github.com/OpenCHAMI/magellan/internal/url" diff --git a/cmd/update.go b/cmd/update.go index 7f7d3b3..83671e4 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -4,7 +4,7 @@ import ( "os" "strings" - magellan "github.com/OpenCHAMI/magellan/internal" + magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/internal/cache/sqlite/sqlite.go b/internal/cache/sqlite/sqlite.go index a3fc0dc..e653aa0 100644 --- a/internal/cache/sqlite/sqlite.go +++ b/internal/cache/sqlite/sqlite.go @@ -3,8 +3,8 @@ package sqlite import ( "fmt" - magellan "github.com/OpenCHAMI/magellan/internal" "github.com/OpenCHAMI/magellan/internal/util" + magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/jmoiron/sqlx" ) diff --git a/internal/collect.go b/pkg/collect.go similarity index 100% rename from internal/collect.go rename to pkg/collect.go diff --git a/internal/scan.go b/pkg/scan.go similarity index 100% rename from internal/scan.go rename to pkg/scan.go diff --git a/internal/update.go b/pkg/update.go similarity index 100% rename from internal/update.go rename to pkg/update.go diff --git a/tests/api_test.go b/tests/api_test.go index e823bfa..999f142 100644 --- a/tests/api_test.go +++ b/tests/api_test.go @@ -22,8 +22,8 @@ import ( "flag" - magellan "github.com/OpenCHAMI/magellan/internal" "github.com/OpenCHAMI/magellan/internal/util" + magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/OpenCHAMI/magellan/pkg/client" "github.com/rs/zerolog/log" ) From 6d0811e86d67ede9d45ca5585844c0c00d225a22 Mon Sep 17 00:00:00 2001 From: Pat Riehecky Date: Mon, 24 Feb 2025 13:07:38 -0600 Subject: [PATCH 06/14] Fix README link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 424fe7f..2135758 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OpenCHAMI Magellan -The `magellan` CLI tool is a Redfish-based, board management controller (BMC) discovery tool designed to scan networks and is written in Go. The tool collects information from BMC nodes using the provided Redfish RESTful API with [`gofish`](https://github.com/stmcginnis/gofish) and loads the queried data into an [SMD](https://github.com/OpenCHAMI/smd/tree/master) instance. The tool strives to be more flexible by implementing multiple methods of discovery to work for a wider range of systems (WIP) and is capable of using independently of other tools or services. +The `magellan` CLI tool is a Redfish-based, board management controller (BMC) discovery tool designed to scan networks and is written in Go. The tool collects information from BMC nodes using the provided Redfish RESTful API with [`gofish`](https://github.com/stmcginnis/gofish) and loads the queried data into an [SMD](https://github.com/OpenCHAMI/smd/) instance. The tool strives to be more flexible by implementing multiple methods of discovery to work for a wider range of systems (WIP) and is capable of using independently of other tools or services. **Note: `magellan` v0.1.0 is incompatible with SMD v2.15.3 and earlier.** From 5bbd0b899823f952827b236e61a15fcba1dacfe0 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Wed, 26 Feb 2025 16:18:42 -0700 Subject: [PATCH 07/14] fix: changed 'update' cmd to use gofish --- cmd/collect.go | 2 +- pkg/collect.go | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cmd/collect.go b/cmd/collect.go index 3e43499..242fd0d 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -55,7 +55,7 @@ var CollectCmd = &cobra.Command{ if concurrency <= 0 { concurrency = mathutil.Clamp(len(scannedResults), 1, 10000) } - err = magellan.CollectInventory(&scannedResults, &magellan.CollectParams{ + _, err = magellan.CollectInventory(&scannedResults, &magellan.CollectParams{ URI: host, Username: username, Password: password, diff --git a/pkg/collect.go b/pkg/collect.go index e52bc60..1e47ca7 100644 --- a/pkg/collect.go +++ b/pkg/collect.go @@ -10,8 +10,8 @@ import ( "net/http" "os" "path" - "strings" "path/filepath" + "strings" "sync" "time" @@ -48,19 +48,20 @@ type CollectParams struct { // // Requests can be made to several of the nodes using a goroutine by setting the q.Concurrency // property value between 1 and 10000. -func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { +func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[string]any, error) { // check for available remote assets found from scan if assets == nil { - return fmt.Errorf("no assets found") + return nil, fmt.Errorf("no assets found") } if len(*assets) <= 0 { - return fmt.Errorf("no assets found") + return nil, fmt.Errorf("no assets found") } // collect bmc information asynchronously var ( offset = 0 wg sync.WaitGroup + collection = make([]map[string]any, 0) found = make([]string, 0, len(*assets)) done = make(chan struct{}, params.Concurrency+1) chanAssets = make(chan RemoteAsset, params.Concurrency+1) @@ -73,7 +74,7 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { if params.CaCertPath != "" { cacert, err := os.ReadFile(params.CaCertPath) if err != nil { - return fmt.Errorf("failed to read CA cert path: %w", err) + return nil, fmt.Errorf("failed to read CA cert path: %w", err) } certPool := x509.NewCertPool() certPool.AppendCertsFromPEM(cacert) @@ -169,6 +170,9 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { fmt.Printf("%v\n", string(body)) } + // add data output to collections + collection = append(collection, data) + // write JSON data to file if output path is set using hive partitioning strategy if outputPath != "" { var ( @@ -241,7 +245,7 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { wg.Wait() close(done) - return nil + return collection, nil } // FindMACAddressWithIP() returns the MAC address of an ethernet interface with From 853e27c3128115f578b30eee37aa9a3893130a13 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Fri, 7 Mar 2025 17:10:31 -0500 Subject: [PATCH 08/14] feat(secrets): implement SecretStore interface and StaticStore/LocalStore for credential management --- cmd/collect.go | 7 +- cmd/crawl.go | 12 ++- go.mod | 5 +- pkg/collect.go | 19 +++-- pkg/crawler/main.go | 63 ++++++++++++-- pkg/scan.go | 2 +- pkg/secrets/encryption.go | 75 ++++++++++++++++ pkg/secrets/encryption_test.go | 41 +++++++++ pkg/secrets/localstore.go | 129 ++++++++++++++++++++++++++++ pkg/secrets/localstore_test.go | 151 +++++++++++++++++++++++++++++++++ pkg/secrets/main.go | 7 ++ pkg/secrets/staticstore.go | 28 ++++++ tests/compatibility_test.go | 14 ++- 13 files changed, 525 insertions(+), 28 deletions(-) create mode 100644 pkg/secrets/encryption.go create mode 100644 pkg/secrets/encryption_test.go create mode 100644 pkg/secrets/localstore.go create mode 100644 pkg/secrets/localstore_test.go create mode 100644 pkg/secrets/main.go create mode 100644 pkg/secrets/staticstore.go diff --git a/cmd/collect.go b/cmd/collect.go index 242fd0d..c304beb 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -8,6 +8,7 @@ import ( urlx "github.com/OpenCHAMI/magellan/internal/url" magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/OpenCHAMI/magellan/pkg/auth" + "github.com/OpenCHAMI/magellan/pkg/secrets" "github.com/cznic/mathutil" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -55,10 +56,10 @@ var CollectCmd = &cobra.Command{ if concurrency <= 0 { concurrency = mathutil.Clamp(len(scannedResults), 1, 10000) } + // Create a StaticSecretStore to hold the username and password + secrets := secrets.NewStaticStore(username, password) _, err = magellan.CollectInventory(&scannedResults, &magellan.CollectParams{ URI: host, - Username: username, - Password: password, Timeout: timeout, Concurrency: concurrency, Verbose: verbose, @@ -66,7 +67,7 @@ var CollectCmd = &cobra.Command{ OutputPath: outputPath, ForceUpdate: forceUpdate, AccessToken: accessToken, - }) + }, secrets) if err != nil { log.Error().Err(err).Msgf("failed to collect data") } diff --git a/cmd/crawl.go b/cmd/crawl.go index ae61135..e9e91bd 100644 --- a/cmd/crawl.go +++ b/cmd/crawl.go @@ -7,6 +7,7 @@ import ( urlx "github.com/OpenCHAMI/magellan/internal/url" "github.com/OpenCHAMI/magellan/pkg/crawler" + "github.com/OpenCHAMI/magellan/pkg/secrets" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -35,11 +36,14 @@ var CrawlCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { + staticStore := &secrets.StaticStore{ + Username: viper.GetString("crawl.username"), + Password: viper.GetString("crawl.password"), + } systems, err := crawler.CrawlBMCForSystems(crawler.CrawlerConfig{ - URI: args[0], - Username: cmd.Flag("username").Value.String(), - Password: cmd.Flag("password").Value.String(), - Insecure: cmd.Flag("insecure").Value.String() == "true", + URI: args[0], + CredentialStore: staticStore, + Insecure: cmd.Flag("insecure").Value.String() == "true", }) if err != nil { log.Fatalf("Error crawling BMC: %v", err) diff --git a/go.mod b/go.mod index 21ee7de..eb835b0 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,10 @@ require ( golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 ) -require github.com/rs/zerolog v1.33.0 +require ( + github.com/rs/zerolog v1.33.0 + golang.org/x/crypto v0.32.0 +) require ( github.com/google/go-cmp v0.6.0 // indirect diff --git a/pkg/collect.go b/pkg/collect.go index 1e47ca7..ccb1a67 100644 --- a/pkg/collect.go +++ b/pkg/collect.go @@ -17,6 +17,7 @@ import ( "github.com/OpenCHAMI/magellan/pkg/client" "github.com/OpenCHAMI/magellan/pkg/crawler" + "github.com/OpenCHAMI/magellan/pkg/secrets" "github.com/rs/zerolog/log" @@ -48,7 +49,7 @@ type CollectParams struct { // // Requests can be made to several of the nodes using a goroutine by setting the q.Concurrency // property value between 1 and 10000. -func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[string]any, error) { +func CollectInventory(assets *[]RemoteAsset, params *CollectParams, store secrets.SecretStore) ([]map[string]any, error) { // check for available remote assets found from scan if assets == nil { return nil, fmt.Errorf("no assets found") @@ -117,10 +118,9 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin systems []crawler.InventoryDetail managers []crawler.Manager config = crawler.CrawlerConfig{ - URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port), - Username: params.Username, - Password: params.Password, - Insecure: true, + URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port), + CredentialStore: store, + Insecure: true, } ) systems, err := crawler.CrawlBMCForSystems(config) @@ -260,10 +260,15 @@ func FindMACAddressWithIP(config crawler.CrawlerConfig, targetIP net.IP) (string // gofish (at least for now). If there's a need for grabbing more // manager information in the future, we can move the logic into // the crawler. + bmc_creds, err := config.GetUserPass() + if err != nil { + return "", fmt.Errorf("failed to get credentials for URI: %s", config.URI) + } + client, err := gofish.Connect(gofish.ClientConfig{ Endpoint: config.URI, - Username: config.Username, - Password: config.Password, + Username: bmc_creds.Username, + Password: bmc_creds.Password, Insecure: config.Insecure, BasicAuth: true, }) diff --git a/pkg/crawler/main.go b/pkg/crawler/main.go index f9526cd..ddf6357 100644 --- a/pkg/crawler/main.go +++ b/pkg/crawler/main.go @@ -1,19 +1,29 @@ package crawler import ( + "encoding/json" "fmt" "strings" + "github.com/OpenCHAMI/magellan/pkg/secrets" "github.com/rs/zerolog/log" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/redfish" ) type CrawlerConfig struct { - URI string // URI of the BMC - Username string // Username for the BMC - Password string // Password for the BMC - Insecure bool // Whether to ignore SSL errors + URI string // URI of the BMC + Insecure bool // Whether to ignore SSL errors + CredentialStore secrets.SecretStore +} + +func (cc *CrawlerConfig) GetUserPass() (BMCUsernamePassword, error) { + return loadBMCCreds(*cc) +} + +type BMCUsernamePassword struct { + Username string `json:"username"` + Password string `json:"password"` } type EthernetInterface struct { @@ -82,11 +92,20 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) { systems []InventoryDetail rf_systems []*redfish.ComputerSystem ) + // get username and password from secret store + bmc_creds, err := loadBMCCreds(config) + if err != nil { + event := log.Error() + event.Err(err) + event.Msg("failed to load BMC credentials") + return nil, err + } + // initialize gofish client client, err := gofish.Connect(gofish.ClientConfig{ Endpoint: config.URI, - Username: config.Username, - Password: config.Password, + Username: bmc_creds.Username, + Password: bmc_creds.Password, Insecure: config.Insecure, BasicAuth: true, }) @@ -149,12 +168,21 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) { // 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) { + + // get username and password from secret store + bmc_creds, err := loadBMCCreds(config) + if err != nil { + event := log.Error() + event.Err(err) + event.Msg("failed to load BMC credentials") + return nil, err + } // initialize gofish client var managers []Manager client, err := gofish.Connect(gofish.ClientConfig{ Endpoint: config.URI, - Username: config.Username, - Password: config.Password, + Username: bmc_creds.Username, + Password: bmc_creds.Password, Insecure: config.Insecure, BasicAuth: true, }) @@ -344,3 +372,22 @@ func walkManagers(rf_managers []*redfish.Manager, baseURI string) ([]Manager, er } return managers, nil } + +func loadBMCCreds(config CrawlerConfig) (BMCUsernamePassword, error) { + creds, err := config.CredentialStore.GetSecretByID(config.URI) + if err != nil { + event := log.Error() + event.Err(err) + event.Msg("failed to get credentials from secret store") + return BMCUsernamePassword{}, err + } + var bmc_creds BMCUsernamePassword + err = json.Unmarshal([]byte(creds), &bmc_creds) + if err != nil { + event := log.Error() + event.Err(err) + event.Msg("failed to unmarshal credentials") + return BMCUsernamePassword{}, err + } + return bmc_creds, nil +} diff --git a/pkg/scan.go b/pkg/scan.go index a88116d..58785ca 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -203,7 +203,7 @@ func rawConnect(address string, protocol string, timeoutSeconds int, keepOpenOnl ) // try to conntect to host (expects host in format [10.0.0.0]:443) - target := fmt.Sprintf("%s:%s", uri.Hostname(), uri.Port()) + target := net.JoinHostPort(uri.Hostname(), uri.Port()) conn, err := net.DialTimeout(protocol, target, timeoutDuration) if err != nil { asset.State = false diff --git a/pkg/secrets/encryption.go b/pkg/secrets/encryption.go new file mode 100644 index 0000000..6faa737 --- /dev/null +++ b/pkg/secrets/encryption.go @@ -0,0 +1,75 @@ +package secrets + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" +) + +// Derive a unique AES key per SecretID using HKDF +func deriveAESKey(masterKey []byte, secretID string) []byte { + salt := []byte(secretID) + hkdf := hkdf.New(sha256.New, masterKey, salt, nil) + derivedKey := make([]byte, 32) // AES-256 key + io.ReadFull(hkdf, derivedKey) + return derivedKey +} + +// Encrypt data using AES-GCM +func encryptAESGCM(key, plaintext []byte) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, aesGCM.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return "", err + } + + ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) + return hex.EncodeToString(ciphertext), nil +} + +// Decrypt data using AES-GCM +func decryptAESGCM(key []byte, encryptedData string) (string, error) { + data, err := hex.DecodeString(encryptedData) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonceSize := aesGCM.NonceSize() + if len(data) < nonceSize { + return "", fmt.Errorf("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", err + } + + return string(plaintext), nil +} diff --git a/pkg/secrets/encryption_test.go b/pkg/secrets/encryption_test.go new file mode 100644 index 0000000..bc1919b --- /dev/null +++ b/pkg/secrets/encryption_test.go @@ -0,0 +1,41 @@ +package secrets + +import ( + "testing" +) + +func TestDeriveAESKey(t *testing.T) { + masterKey := []byte("testmasterkey") + secretID := "mySecretID" + key1 := deriveAESKey(masterKey, secretID) + key2 := deriveAESKey(masterKey, secretID) + + if len(key1) != 32 { + t.Errorf("derived key should be 32 bytes, got %d", len(key1)) + } + if string(key1) != string(key2) { + t.Errorf("keys derived from same secretID should match") + } +} + +func TestEncryptDecryptAESGCM(t *testing.T) { + masterKey := []byte("anotherTestMasterKey") + secretID := "testSecret" + plaintext := "Hello, secrets!" + + key := deriveAESKey(masterKey, secretID) + + encrypted, err := encryptAESGCM(key, []byte(plaintext)) + if err != nil { + t.Fatalf("encryption failed: %v", err) + } + + decrypted, err := decryptAESGCM(key, encrypted) + if err != nil { + t.Fatalf("decryption failed: %v", err) + } + + if decrypted != plaintext { + t.Errorf("expected %q, got %q", plaintext, decrypted) + } +} diff --git a/pkg/secrets/localstore.go b/pkg/secrets/localstore.go new file mode 100644 index 0000000..76fd136 --- /dev/null +++ b/pkg/secrets/localstore.go @@ -0,0 +1,129 @@ +package secrets + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "sync" +) + +// Structure to store encrypted secrets in a JSON file +type LocalSecretStore struct { + mu sync.RWMutex + masterKey []byte + filename string + Secrets map[string]string `json:"secrets"` +} + +func NewLocalSecretStore(masterKeyHex, filename string, create bool) (*LocalSecretStore, error) { + var secrets map[string]string + + masterKey, err := hex.DecodeString(masterKeyHex) + if err != nil { + return nil, fmt.Errorf("unable to generate masterkey from hex representation: %v", err) + } + + if _, err := os.Stat(filename); os.IsNotExist(err) { + if !create { + return nil, fmt.Errorf("file %s does not exist", filename) + } + file, err := os.Create(filename) + if err != nil { + return nil, fmt.Errorf("unable to create file %s: %v", filename, err) + } + file.Close() + secrets = make(map[string]string) + } + + if secrets == nil { + secrets, err = loadSecrets(filename) + if err != nil { + return nil, fmt.Errorf("unable to load secrets from file: %v", err) + } + } + + return &LocalSecretStore{ + masterKey: masterKey, + filename: filename, + Secrets: secrets, + }, nil +} + +// GenerateMasterKey creates a 32-byte random key and returns it as a hex string. +func GenerateMasterKey() (string, error) { + key := make([]byte, 32) // 32 bytes for AES-256 + _, err := rand.Read(key) + if err != nil { + return "", err + } + return hex.EncodeToString(key), nil +} + +// GetSecretByID decrypts the secret using the master key and returns it +func (l *LocalSecretStore) GetSecretByID(secretID string) (string, error) { + l.mu.RLock() + encrypted, exists := l.Secrets[secretID] + l.mu.RUnlock() + if !exists { + return "", fmt.Errorf("no secret found for %s", secretID) + } + + derivedKey := deriveAESKey(l.masterKey, secretID) + return decryptAESGCM(derivedKey, encrypted) +} + +// StoreSecretByID encrypts the secret using the master key and stores it in the JSON file +func (l *LocalSecretStore) StoreSecretByID(secretID, secret string) error { + derivedKey := deriveAESKey(l.masterKey, secretID) + encryptedSecret, err := encryptAESGCM(derivedKey, []byte(secret)) + if err != nil { + return err + } + + l.mu.Lock() + l.Secrets[secretID] = encryptedSecret + err = saveSecrets(l.filename, l.Secrets) + l.mu.Unlock() + return err +} + +// ListSecrets returns a copy of secret IDs to secrets stored in memory +func (l *LocalSecretStore) ListSecrets() (map[string]string, error) { + l.mu.RLock() + defer l.mu.RUnlock() + + secretsCopy := make(map[string]string) + for key, value := range l.Secrets { + secretsCopy[key] = value + } + return secretsCopy, nil +} + +// Saves secrets back to the JSON file +func saveSecrets(jsonFile string, store map[string]string) error { + file, err := os.OpenFile(jsonFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + return encoder.Encode(store) +} + +// Loads the secrets JSON file +func loadSecrets(jsonFile string) (map[string]string, error) { + file, err := os.Open(jsonFile) + if err != nil { + return nil, fmt.Errorf("unable to open secret file %s:%v", jsonFile, err) + } + defer file.Close() + + store := make(map[string]string) + decoder := json.NewDecoder(file) + err = decoder.Decode(&store) + return store, err +} diff --git a/pkg/secrets/localstore_test.go b/pkg/secrets/localstore_test.go new file mode 100644 index 0000000..4009946 --- /dev/null +++ b/pkg/secrets/localstore_test.go @@ -0,0 +1,151 @@ +package secrets + +import ( + "encoding/hex" + "os" + "testing" +) + +func TestNewLocalSecretStore(t *testing.T) { + masterKey, err := GenerateMasterKey() + if err != nil { + t.Fatalf("Failed to generate master key: %v", err) + } + + filename := "test_secrets.json" + defer os.Remove(filename) + + store, err := NewLocalSecretStore(masterKey, filename, true) + if err != nil { + t.Fatalf("Failed to create LocalSecretStore: %v", err) + } + + if store.filename != filename { + t.Errorf("Expected filename %s, got %s", filename, store.filename) + } + + if hex.EncodeToString(store.masterKey) != masterKey { + t.Errorf("Expected master key %s, got %s", masterKey, hex.EncodeToString(store.masterKey)) + } +} + +func TestGenerateMasterKey(t *testing.T) { + key, err := GenerateMasterKey() + if err != nil { + t.Fatalf("Failed to generate master key: %v", err) + } + + if len(key) != 64 { // 32 bytes in hex representation + t.Errorf("Expected key length 64, got %d", len(key)) + } +} + +func TestStoreAndGetSecretByID(t *testing.T) { + masterKey, err := GenerateMasterKey() + if err != nil { + t.Fatalf("Failed to generate master key: %v", err) + } + + filename := "test_secrets.json" + defer os.Remove(filename) + + store, err := NewLocalSecretStore(masterKey, filename, true) + if err != nil { + t.Fatalf("Failed to create LocalSecretStore: %v", err) + } + + secretID := "test_secret" + secretValue := "my_secret_value" + + err = store.StoreSecretByID(secretID, secretValue) + if err != nil { + t.Fatalf("Failed to store secret: %v", err) + } + + retrievedSecret, err := store.GetSecretByID(secretID) + if err != nil { + t.Fatalf("Failed to get secret: %v", err) + } + + if retrievedSecret != secretValue { + t.Errorf("Expected secret value %s, got %s", secretValue, retrievedSecret) + } +} + +func TestStoreAndGetSecretJSON(t *testing.T) { + masterKey, err := GenerateMasterKey() + if err != nil { + t.Fatalf("Failed to generate master key: %v", err) + } + + filename := "test_secrets.json" + defer os.Remove(filename) + + store, err := NewLocalSecretStore(masterKey, filename, true) + if err != nil { + t.Fatalf("Failed to create LocalSecretStore: %v", err) + } + + secretID := "json_creds" + jsonSecret := `{"username":"testUser","password":"testPass"}` + + if err := store.StoreSecretByID(secretID, jsonSecret); err != nil { + t.Fatalf("Failed to store JSON secret: %v", err) + } + + retrieved, err := store.GetSecretByID(secretID) + if err != nil { + t.Fatalf("Failed to get JSON secret by ID: %v", err) + } + + if retrieved != jsonSecret { + t.Errorf("Expected %s, got %s", jsonSecret, retrieved) + } +} + +func TestListSecrets(t *testing.T) { + masterKey, err := GenerateMasterKey() + if err != nil { + t.Fatalf("Failed to generate master key: %v", err) + } + + filename := "test_secrets.json" + defer os.Remove(filename) + + store, err := NewLocalSecretStore(masterKey, filename, true) + if err != nil { + t.Fatalf("Failed to create LocalSecretStore: %v", err) + } + + secretID1 := "test_secret_1" + secretValue1 := "my_secret_value_1" + secretID2 := "test_secret_2" + secretValue2 := "my_secret_value_2" + + err = store.StoreSecretByID(secretID1, secretValue1) + if err != nil { + t.Fatalf("Failed to store secret: %v", err) + } + + err = store.StoreSecretByID(secretID2, secretValue2) + if err != nil { + t.Fatalf("Failed to store secret: %v", err) + } + + secrets, err := store.ListSecrets() + if err != nil { + t.Fatalf("Failed to list secrets: %v", err) + } + + if len(secrets) != 2 { + t.Errorf("Expected 2 secrets, got %d", len(secrets)) + } + + if secrets[secretID1] != store.Secrets[secretID1] { + t.Errorf("Expected secret value %s, got %s", store.Secrets[secretID1], secrets[secretID1]) + } + + if secrets[secretID2] != store.Secrets[secretID2] { + t.Errorf("Expected secret value %s, got %s", store.Secrets[secretID2], secrets[secretID2]) + } +} diff --git a/pkg/secrets/main.go b/pkg/secrets/main.go new file mode 100644 index 0000000..5925d53 --- /dev/null +++ b/pkg/secrets/main.go @@ -0,0 +1,7 @@ +package secrets + +type SecretStore interface { + GetSecretByID(secretID string) (string, error) + StoreSecretByID(secretID, secret string) error + ListSecrets() (map[string]string, error) +} diff --git a/pkg/secrets/staticstore.go b/pkg/secrets/staticstore.go new file mode 100644 index 0000000..3e77870 --- /dev/null +++ b/pkg/secrets/staticstore.go @@ -0,0 +1,28 @@ +package secrets + +import "fmt" + +type StaticStore struct { + Username string + Password string +} + +// NewStaticStore creates a new StaticStore with the given username and password. +func NewStaticStore(username, password string) *StaticStore { + return &StaticStore{ + Username: username, + Password: password, + } +} + +func (s *StaticStore) GetSecretByID(secretID string) (string, error) { + return fmt.Sprintf(`{"username":"%s","password":"%s"}`, s.Username, s.Password), nil +} +func (s *StaticStore) StoreSecretByID(secretID, secret string) error { + return nil +} +func (s *StaticStore) ListSecrets() (map[string]string, error) { + return map[string]string{ + "static_creds": fmt.Sprintf(`{"username":"%s","password":"%s"}`, s.Username, s.Password), + }, nil +} diff --git a/tests/compatibility_test.go b/tests/compatibility_test.go index ce2e876..dfcc5e5 100644 --- a/tests/compatibility_test.go +++ b/tests/compatibility_test.go @@ -16,6 +16,7 @@ import ( "github.com/OpenCHAMI/magellan/pkg/client" "github.com/OpenCHAMI/magellan/pkg/crawler" + "github.com/OpenCHAMI/magellan/pkg/secrets" ) var ( @@ -126,12 +127,17 @@ func TestExpectedOutput(t *testing.T) { t.Fatalf("failed while waiting for emulator: %v", err) } + // initialize a credential store + staticStore := &secrets.StaticStore{ + Username: *username, + Password: *password, + } + systems, err := crawler.CrawlBMCForSystems( crawler.CrawlerConfig{ - URI: *host, - Username: *username, - Password: *password, - Insecure: true, + URI: *host, + CredentialStore: staticStore, + Insecure: true, }, ) From 1fc366eee28e828c06c5c4fa01b1aa38003b1c35 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 30 Jan 2025 08:43:42 -0700 Subject: [PATCH 09/14] bugfix: fixed URL param not being set for UpdateFirmwareRemote --- cmd/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/update.go b/cmd/update.go index 83671e4..65e74c5 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -67,7 +67,7 @@ var updateCmd = &cobra.Command{ TransferProtocol: strings.ToUpper(transferProtocol), Insecure: Insecure, CollectParams: magellan.CollectParams{ - URI: host, + URI: arg, Username: username, Password: password, Timeout: timeout, From a270d68e4b2c4598c663cfe2397d8bf99b5b9e86 Mon Sep 17 00:00:00 2001 From: David Allen Date: Thu, 6 Feb 2025 13:06:37 -0700 Subject: [PATCH 10/14] chore: fix critical dependabot issues by updating crypto --- go.mod | 5 +++++ go.sum | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/go.mod b/go.mod index eb835b0..e342cb2 100644 --- a/go.mod +++ b/go.mod @@ -52,8 +52,13 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect +<<<<<<< HEAD golang.org/x/crypto v0.31.0 // indirect golang.org/x/sys v0.28.0 // indirect +======= + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/sys v0.29.0 // indirect +>>>>>>> 3b85dd3 (chore: fix critical dependabot issues by updating crypto) golang.org/x/text v0.21.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 9c85790..16443ce 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +<<<<<<< HEAD golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +======= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +>>>>>>> 3b85dd3 (chore: fix critical dependabot issues by updating crypto) golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -151,8 +158,15 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +<<<<<<< HEAD golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +======= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +>>>>>>> 3b85dd3 (chore: fix critical dependabot issues by updating crypto) golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -165,7 +179,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +<<<<<<< HEAD golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +======= +>>>>>>> 3b85dd3 (chore: fix critical dependabot issues by updating crypto) golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= From 1477da96931bd7e4509bf75897d0fb716c7d8cb6 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 20 Feb 2025 17:09:21 -0700 Subject: [PATCH 11/14] refactor: moved internal functions to pkg and updated refs --- pkg/update.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/pkg/update.go b/pkg/update.go index ccbed6b..b140a59 100644 --- a/pkg/update.go +++ b/pkg/update.go @@ -1,11 +1,20 @@ package magellan import ( +<<<<<<< HEAD "fmt" "net/url" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/redfish" +======= + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/OpenCHAMI/magellan/pkg/client" +>>>>>>> 81116ec (refactor: moved internal functions to pkg and updated refs) ) type UpdateParams struct { @@ -14,12 +23,16 @@ type UpdateParams struct { FirmwareVersion string Component string TransferProtocol string +<<<<<<< HEAD Insecure bool +======= +>>>>>>> 81116ec (refactor: moved internal functions to pkg and updated refs) } // 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. +<<<<<<< HEAD // 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 @@ -29,12 +42,15 @@ type UpdateParams struct { // q.URI https://192.168.23.40 // q.TransferProtocol TFTP // q.FirmwarePath http://192.168.23.19:1337/obmc-phosphor-image.static.mtd.tar +======= +>>>>>>> 81116ec (refactor: moved internal functions to pkg and updated refs) 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) } +<<<<<<< HEAD // Connect to the Redfish service using gofish client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: q.Username, Password: q.Password, Insecure: q.Insecure}) @@ -61,6 +77,34 @@ func UpdateFirmwareRemote(q *UpdateParams) error { return fmt.Errorf("firmware update failed: %w", err) } fmt.Println("Firmware update initiated successfully.") +======= + 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) + if err != nil { + return fmt.Errorf("failed to marshal data: %v", err) + } + res, body, err := client.MakeRequest(nil, updateUrl, "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)", updateUrl) + } + if len(body) > 0 { + fmt.Printf("%d: %v\n", res.StatusCode, string(body)) + } +>>>>>>> 81116ec (refactor: moved internal functions to pkg and updated refs) return nil } @@ -70,6 +114,7 @@ func GetUpdateStatus(q *UpdateParams) error { if err != nil { return fmt.Errorf("failed to parse URI: %w", err) } +<<<<<<< HEAD // Connect to the Redfish service using gofish client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: q.Username, Password: q.Password, Insecure: q.Insecure}) @@ -88,5 +133,20 @@ func GetUpdateStatus(q *UpdateParams) error { status := updateService.Status fmt.Printf("Update Status: %v\n", status) +======= + uri.User = url.UserPassword(q.Username, q.Password) + updateUrl := fmt.Sprintf("%s/redfish/v1/UpdateService", uri.String()) + res, body, err := client.MakeRequest(nil, updateUrl, "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)", updateUrl) + } else if res.StatusCode != http.StatusOK { + return fmt.Errorf("returned status code %d", res.StatusCode) + } + if len(body) > 0 { + fmt.Printf("%v\n", string(body)) + } +>>>>>>> 81116ec (refactor: moved internal functions to pkg and updated refs) return nil } From 524a38a47527ce4ae26ab7f6649852124ea00927 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Wed, 26 Feb 2025 16:18:42 -0700 Subject: [PATCH 12/14] collect: return collection output from CollectInventory() --- go.sum | 10 ++++++++++ pkg/update.go | 48 ------------------------------------------------ 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/go.sum b/go.sum index 16443ce..3bd122e 100644 --- a/go.sum +++ b/go.sum @@ -124,11 +124,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= <<<<<<< HEAD +<<<<<<< HEAD golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= ======= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +======= +>>>>>>> 97a569d (collect: return collection output from CollectInventory()) golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= >>>>>>> 3b85dd3 (chore: fix critical dependabot issues by updating crypto) @@ -159,11 +162,14 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= <<<<<<< HEAD +<<<<<<< HEAD golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= ======= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +======= +>>>>>>> 97a569d (collect: return collection output from CollectInventory()) golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= >>>>>>> 3b85dd3 (chore: fix critical dependabot issues by updating crypto) @@ -180,9 +186,13 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= <<<<<<< HEAD +<<<<<<< HEAD golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= ======= >>>>>>> 3b85dd3 (chore: fix critical dependabot issues by updating crypto) +======= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +>>>>>>> 97a569d (collect: return collection output from CollectInventory()) golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/pkg/update.go b/pkg/update.go index b140a59..e031c75 100644 --- a/pkg/update.go +++ b/pkg/update.go @@ -32,7 +32,6 @@ 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. -<<<<<<< HEAD // 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 @@ -42,15 +41,12 @@ type UpdateParams struct { // q.URI https://192.168.23.40 // q.TransferProtocol TFTP // q.FirmwarePath http://192.168.23.19:1337/obmc-phosphor-image.static.mtd.tar -======= ->>>>>>> 81116ec (refactor: moved internal functions to pkg and updated refs) 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) } -<<<<<<< HEAD // Connect to the Redfish service using gofish client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: q.Username, Password: q.Password, Insecure: q.Insecure}) @@ -77,34 +73,6 @@ func UpdateFirmwareRemote(q *UpdateParams) error { return fmt.Errorf("firmware update failed: %w", err) } fmt.Println("Firmware update initiated successfully.") -======= - 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) - if err != nil { - return fmt.Errorf("failed to marshal data: %v", err) - } - res, body, err := client.MakeRequest(nil, updateUrl, "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)", updateUrl) - } - if len(body) > 0 { - fmt.Printf("%d: %v\n", res.StatusCode, string(body)) - } ->>>>>>> 81116ec (refactor: moved internal functions to pkg and updated refs) return nil } @@ -114,7 +82,6 @@ func GetUpdateStatus(q *UpdateParams) error { if err != nil { return fmt.Errorf("failed to parse URI: %w", err) } -<<<<<<< HEAD // Connect to the Redfish service using gofish client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: q.Username, Password: q.Password, Insecure: q.Insecure}) @@ -133,20 +100,5 @@ func GetUpdateStatus(q *UpdateParams) error { status := updateService.Status fmt.Printf("Update Status: %v\n", status) -======= - uri.User = url.UserPassword(q.Username, q.Password) - updateUrl := fmt.Sprintf("%s/redfish/v1/UpdateService", uri.String()) - res, body, err := client.MakeRequest(nil, updateUrl, "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)", updateUrl) - } else if res.StatusCode != http.StatusOK { - return fmt.Errorf("returned status code %d", res.StatusCode) - } - if len(body) > 0 { - fmt.Printf("%v\n", string(body)) - } ->>>>>>> 81116ec (refactor: moved internal functions to pkg and updated refs) return nil } From 8bb8e4a49aaff5483a6ad1da31da085f9a4e6528 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Fri, 7 Mar 2025 17:10:31 -0500 Subject: [PATCH 13/14] feat(secrets): implement SecretStore interface and StaticStore/LocalStore for credential management --- go.mod | 4 ---- 1 file changed, 4 deletions(-) diff --git a/go.mod b/go.mod index e342cb2..9b83a07 100644 --- a/go.mod +++ b/go.mod @@ -52,13 +52,9 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect -<<<<<<< HEAD golang.org/x/crypto v0.31.0 // indirect golang.org/x/sys v0.28.0 // indirect -======= - golang.org/x/crypto v0.32.0 // indirect golang.org/x/sys v0.29.0 // indirect ->>>>>>> 3b85dd3 (chore: fix critical dependabot issues by updating crypto) golang.org/x/text v0.21.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect From 9615f523f11ca63c04cb144989a87930b70ffd64 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Thu, 6 Feb 2025 17:25:56 -0500 Subject: [PATCH 14/14] feat: enhance firmware update functionality and add BMC identification support --- pkg/update.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/pkg/update.go b/pkg/update.go index e031c75..757b455 100644 --- a/pkg/update.go +++ b/pkg/update.go @@ -1,20 +1,14 @@ package magellan import ( -<<<<<<< HEAD "fmt" "net/url" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/redfish" -======= - "encoding/json" - "fmt" - "net/http" - "net/url" - "github.com/OpenCHAMI/magellan/pkg/client" ->>>>>>> 81116ec (refactor: moved internal functions to pkg and updated refs) + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/redfish" ) type UpdateParams struct { @@ -23,10 +17,7 @@ type UpdateParams struct { FirmwareVersion string Component string TransferProtocol string -<<<<<<< HEAD Insecure bool -======= ->>>>>>> 81116ec (refactor: moved internal functions to pkg and updated refs) } // UpdateFirmwareRemote() uses 'gofish' to update the firmware of a BMC node.