diff --git a/cmd/collect.go b/cmd/collect.go index 2cf6819..4c7d910 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -18,6 +18,9 @@ var ( forceUpdate bool ) +// The `collect` command fetches data from a collection of BMC nodes. +// This command should be ran after the `scan` to find available hosts +// on a subnet. var collectCmd = &cobra.Command{ Use: "collect", Short: "Query information about BMC", diff --git a/cmd/root.go b/cmd/root.go index 2d27bee..22d75d0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,3 +1,17 @@ +// The cmd package implements the interface for the magellan CLI. The files +// contained in this package only contains implementations for handling CLI +// arguments and passing them to functions within magellan's internal API. +// +// Each CLI subcommand will have at least one corresponding internal file +// with an API routine that implements the command's functionality. The main +// API routine will usually be the first function defined in the fill. +// +// For example: +// +// cmd/scan.go --> internal/scan.go ( magellan.ScanForAssets() ) +// cmd/collect.go --> internal/collect.go ( magellan.CollectAll() ) +// cmd/list.go --> none (doesn't have API call since it's simple) +// cmd/update.go --> internal/update.go ( magellan.UpdateFirmware() ) package cmd import ( @@ -30,11 +44,8 @@ var ( verbose bool ) -// TODO: discover bmc's on network (dora) -// TODO: query bmc component information and store in db (?) -// TODO: send bmc component information to smd -// TODO: set ports to scan automatically with set driver - +// The `root` command doesn't do anything on it's own except display +// a help message and then exits. var rootCmd = &cobra.Command{ Use: "magellan", Short: "Tool for BMC discovery", @@ -47,6 +58,7 @@ var rootCmd = &cobra.Command{ }, } +// This Execute() function is called from main to run the CLI. func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) @@ -54,6 +66,14 @@ func Execute() { } } +// LoadAccessToken() tries to load a JWT string from an environment +// variable, file, or config in that order. If loading the token +// fails with one options, it will fallback to the next option until +// all options are exhausted. +// +// Returns a token as a string with no error if successful. +// Alternatively, returns an empty string with an error if a token is +// not able to be loaded. func LoadAccessToken() (string, error) { // try to load token from env var testToken := os.Getenv("MAGELLAN_ACCESS_TOKEN") @@ -93,12 +113,21 @@ func init() { viper.BindPFlags(rootCmd.Flags()) } +// InitializeConfig() initializes a new config object by loading it +// from a file given a non-empty string. +// +// See the 'LoadConfig' function in 'internal/config' for details. func InitializeConfig() { if configPath != "" { magellan.LoadConfig(configPath) } } +// SetDefaults() resets all of the viper properties back to their +// default values. +// +// TODO: This function should probably be moved to 'internal/config.go' +// instead of in this file. func SetDefaults() { viper.SetDefault("threads", 1) viper.SetDefault("timeout", 30) diff --git a/cmd/scan.go b/cmd/scan.go index 5bb5019..c1162e8 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -25,6 +25,12 @@ var ( disableProbing bool ) +// The `scan` command is usually the first step to using the CLI tool. +// This command will perform a network scan over a subnet by supplying +// a list of subnets, subnet masks, and additional IP address to probe. +// +// See the `ScanForAssets()` function in 'internal/scan.go' for details +// related to the implementation. var scanCmd = &cobra.Command{ Use: "scan", Short: "Scan for BMC nodes on a network", diff --git a/internal/collect.go b/internal/collect.go index f678e42..acdc331 100644 --- a/internal/collect.go +++ b/internal/collect.go @@ -1,3 +1,4 @@ +// Package magellan implements the core routines for the tools. package magellan import ( @@ -50,6 +51,11 @@ type QueryParams struct { AccessToken string } +// This is the main function used to collect information from the BMC nodes via Redfish. +// The function expects a list of hosts found using the `ScanForAssets()` function. +// +// Requests can be made to several of the nodes using a goroutine by setting the q.Concurrency +// property value between 1 and 255. func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) error { // check for available probe states if probeStates == nil { @@ -102,6 +108,7 @@ func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) err if err != nil { l.Log.Errorf("failed to connect to BMC (%v:%v): %v", q.Host, q.Port, err) } + defer gofishClient.Logout() // data to be sent to smd data := map[string]any{ @@ -218,6 +225,7 @@ func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) err return nil } +// TODO: DELETE ME!!! func CollectMetadata(client *bmclib.Client, q *QueryParams) ([]byte, error) { // open BMC session and update driver registry ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout)) @@ -275,6 +283,7 @@ func CollectInventory(client *bmclib.Client, q *QueryParams) ([]byte, error) { return b, nil } +// TODO: DELETE ME!!! func CollectPowerState(client *bmclib.Client, q *QueryParams) ([]byte, error) { ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout)) client.Registry.FilterForCompatible(ctx) @@ -303,6 +312,7 @@ func CollectPowerState(client *bmclib.Client, q *QueryParams) ([]byte, error) { } +// TODO: DELETE ME!!! func CollectUsers(client *bmclib.Client, q *QueryParams) ([]byte, error) { // open BMC session and update driver registry ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout)) @@ -333,11 +343,18 @@ func CollectUsers(client *bmclib.Client, q *QueryParams) ([]byte, error) { return b, nil } +// TODO: DELETE ME!!!q + func CollectBios(client *bmclib.Client, q *QueryParams) ([]byte, error) { b, err := makeRequest(client, client.GetBiosConfiguration, q.Timeout) return b, err } +// CollectEthernetInterfaces() collects all of the ethernet interfaces found +// from all systems from under the "/redfish/v1/Systems" endpoint. +// +// TODO: This function needs to be refactored entirely...if not deleted +// in favor of using crawler.CrawlBM() instead. func CollectEthernetInterfaces(c *gofish.APIClient, q *QueryParams, systemID string) ([]byte, error) { // TODO: add more endpoints to test for ethernet interfaces // /redfish/v1/Chassis/{ChassisID}/NetworkAdapters/{NetworkAdapterId}/NetworkDeviceFunctions/{NetworkDeviceFunctionId}/EthernetInterfaces/{EthernetInterfaceId} @@ -380,6 +397,7 @@ func CollectEthernetInterfaces(c *gofish.APIClient, q *QueryParams, systemID str return b, nil } +// TODO: DELETE ME!!! func CollectChassis(c *gofish.APIClient, q *QueryParams) ([]map[string]any, error) { rfChassis, err := c.Service.Chassis() if err != nil { @@ -402,6 +420,7 @@ func CollectChassis(c *gofish.APIClient, q *QueryParams) ([]map[string]any, erro return chassis, nil } +// TODO: DELETE ME!!! func CollectStorage(c *gofish.APIClient, q *QueryParams) ([]byte, error) { systems, err := c.Service.StorageSystems() if err != nil { @@ -427,19 +446,23 @@ func CollectStorage(c *gofish.APIClient, q *QueryParams) ([]byte, error) { return b, nil } +// CollectSystems pulls system information from each BMC node via Redfish using the +// `gofish` library. +// +// The process of collecting this info is as follows: +// 1. check if system has ethernet interfaces +// 1.a. if yes, create system data and ethernet interfaces JSON +// 1.b. if no, try to get data using manager instead +// 2. check if manager has "ManagerForServices" and "EthernetInterfaces" properties +// 2.a. if yes, query both properties to use in next step +// 2.b. for each service, query its data and add the ethernet interfaces +// 2.c. add the system to list of systems to marshal and return func CollectSystems(c *gofish.APIClient, q *QueryParams) ([]map[string]any, error) { rfSystems, err := c.Service.Systems() if err != nil { return nil, fmt.Errorf("failed to get systems (%v:%v): %v", q.Host, q.Port, err) } - // 1. check if system has ethernet interfaces - // 1.a. if yes, create system data and ethernet interfaces JSON - // 1.b. if no, try to get data using manager instead - // 2. check if manager has "ManagerForServices" and "EthernetInterfaces" properties - // 2.a. if yes, query both properties to use in next step - // 2.b. for each service, query its data and add the ethernet interfaces - // 2.c. add the system to list of systems to marshal and return var systems []map[string]any for _, system := range rfSystems { @@ -605,6 +628,7 @@ func CollectSystems(c *gofish.APIClient, q *QueryParams) ([]map[string]any, erro return systems, nil } +// TODO: DELETE ME!!! func CollectRegisteries(c *gofish.APIClient, q *QueryParams) ([]byte, error) { registries, err := c.Service.Registries() if err != nil { @@ -620,6 +644,7 @@ func CollectRegisteries(c *gofish.APIClient, q *QueryParams) ([]byte, error) { return b, nil } +// TODO: MAYBE DELETE??? func CollectProcessors(q *QueryParams) ([]byte, error) { url := baseRedfishUrl(q) + "/Systems" res, body, err := util.MakeRequest(nil, url, "GET", nil, nil) diff --git a/internal/util/util.go b/internal/util/util.go index a0ba641..6817f4a 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -13,6 +13,11 @@ import ( "time" ) +// PathExists() is a wrapper function that simplifies checking +// if a file or directory already exists at the provided path. +// +// Returns whether the path exists and no error if successful, +// otherwise, it returns false with an error. func PathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { @@ -24,6 +29,8 @@ func PathExists(path string) (bool, error) { return false, err } +// GetNextIP() returns the next IP address, but does not account +// for net masks. func GetNextIP(ip *net.IP, inc uint) *net.IP { if ip == nil { return &net.IP{} @@ -40,7 +47,14 @@ func GetNextIP(ip *net.IP, inc uint) *net.IP { return &r } -// Generic convenience function used to make HTTP requests. +// MakeRequest() is a wrapper function that condenses simple HTTP +// requests done to a single call. It expects an optional HTTP client, +// URL, HTTP method, request body, and request headers. This function +// is useful when making many requests where only these few arguments +// are changing. +// +// Returns a HTTP response object, response body as byte array, and any +// error that may have occurred with making the request. func MakeRequest(client *http.Client, url string, httpMethod string, body []byte, headers map[string]string) (*http.Response, []byte, error) { // use defaults if no client provided if client == nil { @@ -69,6 +83,12 @@ func MakeRequest(client *http.Client, url string, httpMethod string, body []byte return res, b, err } +// MakeOutputDirectory() creates a new directory at the path argument if +// the path does not exist +// +// TODO: Refactor this function for hive partitioning or possibly move into +// the logging package. +// TODO: Add an option to force overwriting the path. func MakeOutputDirectory(path string) (string, error) { // get the current data + time using Go's stupid formatting t := time.Now() @@ -93,12 +113,27 @@ func MakeOutputDirectory(path string) (string, error) { return final, nil } +// SplitPathForViper() is an utility function to split a path into 3 parts: +// - directory +// - filename +// - extension +// The intent was to break a path into a format that's more easily consumable +// by spf13/viper's API. See the "LoadConfig()" function in internal/config.go +// for more details. +// +// TODO: Rename function to something more generalized. func SplitPathForViper(path string) (string, string, string) { filename := filepath.Base(path) ext := filepath.Ext(filename) return filepath.Dir(path), strings.TrimSuffix(filename, ext), strings.TrimPrefix(ext, ".") } +// FormatErrorList() is a wrapper function that unifies error list formatting +// and makes printing error lists consistent. +// +// NOTE: The error returned IS NOT an error in itself and may be a bit misleading. +// Instead, it is a single condensed error composed of all of the errors included +// in the errList argument. func FormatErrorList(errList []error) error { var err error for i, e := range errList { @@ -108,6 +143,9 @@ func FormatErrorList(errList []error) error { return err } +// HasErrors() is a simple wrapper function to check if an error list contains +// errors. Having a function that clearly states its purpose helps to improve +// readibility although it may seem pointless. func HasErrors(errList []error) bool { return len(errList) > 0 }