From f7b08da064744ae07719181426c121e43ad86239 Mon Sep 17 00:00:00 2001 From: David Allen Date: Tue, 23 Jul 2024 16:18:21 -0600 Subject: [PATCH] Added more API documentation --- cmd/collect.go | 1 + cmd/list.go | 10 ++- cmd/root.go | 2 +- cmd/update.go | 7 ++ internal/config.go | 10 ++- internal/login.go | 11 +++ internal/scan.go | 189 +++++++++++++++++++++++++-------------------- internal/update.go | 10 ++- 8 files changed, 150 insertions(+), 90 deletions(-) diff --git a/cmd/collect.go b/cmd/collect.go index fa4b7b1..3641a55 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -95,6 +95,7 @@ func init() { // set flags to only be used together collectCmd.MarkFlagsRequiredTogether("username", "password") + // bind flags to config properties viper.BindPFlag("collect.driver", collectCmd.Flags().Lookup("driver")) viper.BindPFlag("collect.host", collectCmd.Flags().Lookup("host")) viper.BindPFlag("collect.port", collectCmd.Flags().Lookup("port")) diff --git a/cmd/list.go b/cmd/list.go index b722c81..89cd847 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -12,9 +12,17 @@ import ( "github.com/spf13/cobra" ) +// The `list` command provides an easy way to show what was found +// and stored in a cache database from a scan. The data that's stored +// is what is consumed by the `collect` command with the --cache flag. var listCmd = &cobra.Command{ Use: "list", - Short: "List information from scan", + Short: "List information stored in cache from a scan", + Long: "Prints all of the host and associated data found from performing a scan.\n" + + "See the 'scan' command on how to perform a scan.\n\n" + + "Examples:\n" + + " magellan list\n" + + " magellan list " Run: func(cmd *cobra.Command, args []string) { probeResults, err := sqlite.GetProbeResults(cachePath) if err != nil { diff --git a/cmd/root.go b/cmd/root.go index 22d75d0..40f7407 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -76,7 +76,7 @@ func Execute() { // not able to be loaded. func LoadAccessToken() (string, error) { // try to load token from env var - testToken := os.Getenv("MAGELLAN_ACCESS_TOKEN") + testToken := os.Getenv("ACCESS_TOKEN") if testToken != "" { return testToken, nil } diff --git a/cmd/update.go b/cmd/update.go index 0b27291..87f2282 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -18,9 +18,16 @@ var ( status bool ) +// The `update` command provides an interface to easily update firmware +// using Redfish. It also provides a simple way to check the status of +// an update in-progress. var updateCmd = &cobra.Command{ Use: "update", Short: "Update BMC node firmware", + Long: "Perform an firmware update using Redfish by providing a remote firmware URL and component.\n" + + "Examples:\n" + + " magellan update --host 172.16.0.108 --port 443 --username bmc_username --password bmc_password --firmware-url http://172.16.0.200:8005/firmware/bios/image.RBU --component BIOS\n" + + " magellan update --status --host 172.16.0.108 --port 443 --username bmc_username --password bmc_password", Run: func(cmd *cobra.Command, args []string) { l := log.NewLogger(logrus.New(), logrus.DebugLevel) q := &magellan.UpdateParams{ diff --git a/internal/config.go b/internal/config.go index a9e7b4c..4059269 100644 --- a/internal/config.go +++ b/internal/config.go @@ -7,15 +7,19 @@ import ( "github.com/spf13/viper" ) +// LoadConfig() will load a YAML config file at the specified path. There are some general +// considerations about how this is done with spf13/viper: +// +// 1. There are intentionally no search paths set, so config path has to be set explicitly +// 2. No data will be written to the config file from the tool +// 3. Parameters passed as CLI flags and envirnoment variables should always have +// precedence over values set in the config. func LoadConfig(path string) error { dir, filename, ext := util.SplitPathForViper(path) // fmt.Printf("dir: %s\nfilename: %s\nextension: %s\n", dir, filename, ext) viper.AddConfigPath(dir) viper.SetConfigName(filename) viper.SetConfigType(ext) - // ...no search paths set intentionally, so config has to be set explicitly - // ...also, the config file will not save anything - // ...and finally, parameters passed to CLI have precedence over config values viper.AutomaticEnv() if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { diff --git a/internal/login.go b/internal/login.go index 508a987..a78aa8d 100644 --- a/internal/login.go +++ b/internal/login.go @@ -8,6 +8,17 @@ import ( "github.com/pkg/browser" ) +// Login() initiates the process to retrieve an access token from an identity provider. +// This function is especially designed to work by OPAAL, but will propably be changed +// in the future to be more agnostic. +// +// The 'targetHost' and 'targetPort' parameters should point to the target host/port +// to create a temporary server to receive the access token. If an empty 'targetHost' +// or an invalid port range is passed, then neither of the parameters will be used +// and no server will be started. +// +// Returns an access token as a string if successful and nil error. Otherwise, returns +// an empty string with an error set. func Login(loginUrl string, targetHost string, targetPort int) (string, error) { var accessToken string diff --git a/internal/scan.go b/internal/scan.go index ef4ca41..660aede 100644 --- a/internal/scan.go +++ b/internal/scan.go @@ -19,94 +19,31 @@ type ScannedResult struct { Timestamp time.Time `json:"timestamp"` } -func rawConnect(host string, ports []int, timeout int, keepOpenOnly bool) []ScannedResult { - results := []ScannedResult{} - for _, p := range ports { - result := ScannedResult{ - Host: host, - Port: p, - Protocol: "tcp", - State: false, - Timestamp: time.Now(), - } - t := time.Second * time.Duration(timeout) - port := fmt.Sprint(p) - conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), t) - if err != nil { - result.State = false - // fmt.Println("Connecting error:", err) - } - if conn != nil { - result.State = true - defer conn.Close() - // fmt.Println("Opened", net.JoinHostPort(host, port)) - } - if keepOpenOnly { - if result.State { - results = append(results, result) - } - } else { - results = append(results, result) - } - } - - return results -} - -func GenerateHosts(subnet string, subnetMask *net.IP) []string { - if subnet == "" || subnetMask == nil { - return nil - } - - // convert subnets from string to net.IP - subnetIp := net.ParseIP(subnet) - if subnetIp == nil { - // try parse CIDR instead - ip, network, err := net.ParseCIDR(subnet) - if err != nil { - return nil - } - subnetIp = ip - if network != nil { - t := net.IP(network.Mask) - subnetMask = &t - } - } - - mask := net.IPMask(subnetMask.To4()) - - // if no subnet mask, use a default 24-bit mask (for now) - return generateHosts(&subnetIp, &mask) -} - -func generateHosts(ip *net.IP, mask *net.IPMask) []string { - // get all IP addresses in network - ones, _ := mask.Size() - hosts := []string{} - end := int(math.Pow(2, float64((32-ones)))) - 1 - for i := 0; i < end; i++ { - // ip[3] = byte(i) - ip = util.GetNextIP(ip, 1) - if ip == nil { - continue - } - // host := fmt.Sprintf("%v.%v.%v.%v", (*ip)[0], (*ip)[1], (*ip)[2], (*ip)[3]) - // fmt.Printf("host: %v\n", ip.String()) - hosts = append(hosts, ip.String()) - } - return hosts -} - -func ScanForAssets(hosts []string, ports []int, threads int, timeout int, disableProbing bool, verbose bool) []ScannedResult { +// ScanForAssets() performs a net scan on a network to find available services +// running. The function expects a list of hosts and ports to make requests. +// Note that each all ports will be used per host. +// +// This function runs in a goroutine with the "concurrency" flag setting the +// number of concurrent requests. Only one request is made to each BMC node +// at a time, but setting a value greater than 1 with enable the requests +// to be made concurrently. +// +// If the "disableProbing" flag is set, then the function will skip the extra +// HTTP request made to check if the response was from a Redfish service. +// Otherwise, not receiving a 200 OK response code from the HTTP request will +// remove the service from being stored in the list of scanned results. +// +// Returns a list of scanned results to be stored in cache (but isn't doing here). +func ScanForAssets(hosts []string, ports []int, concurrency int, timeout int, disableProbing bool, verbose bool) []ScannedResult { var ( results = make([]ScannedResult, 0, len(hosts)) - done = make(chan struct{}, threads+1) - chanHost = make(chan string, threads+1) + done = make(chan struct{}, concurrency+1) + chanHost = make(chan string, concurrency+1) ) var wg sync.WaitGroup - wg.Add(threads) - for i := 0; i < threads; i++ { + wg.Add(concurrency) + for i := 0; i < concurrency; i++ { go func() { for { host, ok := <-chanHost @@ -161,6 +98,92 @@ func ScanForAssets(hosts []string, ports []int, threads int, timeout int, disabl return results } +// GenerateHosts() builds a list of hosts to scan using the "subnet" +// and "subnetMask" arguments passed. The function is capable of +// distinguishing between IP formats: a subnet with just an IP address (172.16.0.0) and +// a subnet with IP address and CIDR (172.16.0.0/24). +// +// NOTE: If a IP address is provided with CIDR, then the "subnetMask" +// parameter will be ignored. If neither is provided, then the default +// subnet mask will be used instead. +func GenerateHosts(subnet string, subnetMask *net.IP) []string { + if subnet == "" || subnetMask == nil { + return nil + } + + // convert subnets from string to net.IP + subnetIp := net.ParseIP(subnet) + if subnetIp == nil { + // try parse CIDR instead + ip, network, err := net.ParseCIDR(subnet) + if err != nil { + return nil + } + subnetIp = ip + if network != nil { + t := net.IP(network.Mask) + subnetMask = &t + } + } + + mask := net.IPMask(subnetMask.To4()) + + // if no subnet mask, use a default 24-bit mask (for now) + return generateHosts(&subnetIp, &mask) +} + func GetDefaultPorts() []int { return []int{HTTPS_PORT} } + +func rawConnect(host string, ports []int, timeout int, keepOpenOnly bool) []ScannedResult { + results := []ScannedResult{} + for _, p := range ports { + result := ScannedResult{ + Host: host, + Port: p, + Protocol: "tcp", + State: false, + Timestamp: time.Now(), + } + t := time.Second * time.Duration(timeout) + port := fmt.Sprint(p) + conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), t) + if err != nil { + result.State = false + // fmt.Println("Connecting error:", err) + } + if conn != nil { + result.State = true + defer conn.Close() + // fmt.Println("Opened", net.JoinHostPort(host, port)) + } + if keepOpenOnly { + if result.State { + results = append(results, result) + } + } else { + results = append(results, result) + } + } + + return results +} + +func generateHosts(ip *net.IP, mask *net.IPMask) []string { + // get all IP addresses in network + ones, _ := mask.Size() + hosts := []string{} + end := int(math.Pow(2, float64((32-ones)))) - 1 + for i := 0; i < end; i++ { + // ip[3] = byte(i) + ip = util.GetNextIP(ip, 1) + if ip == nil { + continue + } + // host := fmt.Sprintf("%v.%v.%v.%v", (*ip)[0], (*ip)[1], (*ip)[2], (*ip)[3]) + // fmt.Printf("host: %v\n", ip.String()) + hosts = append(hosts, ip.String()) + } + return hosts +} diff --git a/internal/update.go b/internal/update.go index e6ed6b3..35ed4c4 100644 --- a/internal/update.go +++ b/internal/update.go @@ -26,8 +26,14 @@ type UpdateParams struct { 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 +// UpdateFirmware() uses 'bmc-toolbox/bmclib' 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. +// +// NOTE: Multipart HTTP updating may not work since older verions of OpenBMC, which bmclib +// uses underneath, did not support support multipart updates. This was changed with the +// inclusion of support for MultipartHttpPushUri in OpenBMC (https://gerrit.openbmc.org/c/openbmc/bmcweb/+/32174). +// Also, related to bmclib: 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")