From c950532e8836f68ba804e74e154516cca6c96c51 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Fri, 28 Mar 2025 13:12:38 -0600 Subject: [PATCH] refactor: improvements to CLI and update pkg --- cmd/collect.go | 24 ++++++++++++------------ cmd/crawl.go | 12 ++++-------- cmd/list.go | 11 ++++++----- cmd/root.go | 8 ++++---- cmd/scan.go | 40 +++++++++++++++++++++++----------------- cmd/secrets.go | 32 ++++++++++++++++++-------------- cmd/update.go | 40 +++++++++++++++++----------------------- pkg/update.go | 6 ++---- 8 files changed, 86 insertions(+), 87 deletions(-) diff --git a/cmd/collect.go b/cmd/collect.go index 7223a75..9978ae0 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -19,17 +19,19 @@ import ( // This command should be ran after the `scan` to find available hosts // on a subnet. var CollectCmd = &cobra.Command{ - Use: "collect", + Use: "collect", + Example: ` // basic collect after scan without making a follow-up request + magellan collect --cache ./assets.db --cacert ochami.pem -o ./logs -t 30 + + // set username and password for all nodes and make request to specified host + magellan collect --host https://smd.openchami.cluster -u $bmc_username -p $bmc_password + + // run a collect using secrets manager with fallback username and password + export MASTER_KEY=$(magellan secrets generatekey) + magellan secrets store $node_creds_json -f nodes.json + magellan collect --host https://smd.openchami.cluster -u $fallback_bmc_username -p $fallback_bmc_password`, Short: "Collect system information by interrogating BMC node", - Long: "Send request(s) to a collection of hosts running Redfish services found stored from the 'scan' in cache.\n" + - "See the 'scan' command on how to perform a scan.\n\n" + - "Examples:\n" + - " magellan collect --cache ./assets.db --output ./logs --timeout 30 --cacert cecert.pem\n" + - " magellan collect --host smd.example.com --port 27779 --username $username --password $password\n\n" + - // example using `collect` - " export MASTER_KEY=$(magellan secrets generatekey)\n" + - " magellan secrets store $node_creds_json -f nodes.json" + - " magellan collect --host openchami.cluster --username $username --password $password \\\n", + Long: "Send request(s) to a collection of hosts running Redfish services found stored from the 'scan' in cache.\nSee the 'scan' command on how to perform a scan.", Run: func(cmd *cobra.Command, args []string) { // get probe states stored in db from scan scannedResults, err := sqlite.GetScannedAssets(cachePath) @@ -110,8 +112,6 @@ func init() { // bind flags to config properties checkBindFlagError(viper.BindPFlag("collect.host", CollectCmd.Flags().Lookup("host"))) - checkBindFlagError(viper.BindPFlag("collect.username", CollectCmd.Flags().Lookup("username"))) - checkBindFlagError(viper.BindPFlag("collect.password", CollectCmd.Flags().Lookup("password"))) checkBindFlagError(viper.BindPFlag("collect.scheme", CollectCmd.Flags().Lookup("scheme"))) checkBindFlagError(viper.BindPFlag("collect.protocol", CollectCmd.Flags().Lookup("protocol"))) checkBindFlagError(viper.BindPFlag("collect.output", CollectCmd.Flags().Lookup("output"))) diff --git a/cmd/crawl.go b/cmd/crawl.go index 0e2984d..e059663 100644 --- a/cmd/crawl.go +++ b/cmd/crawl.go @@ -17,13 +17,11 @@ import ( // specfic inventory detail. This command only expects host names and does // not require a scan to be performed beforehand. var CrawlCmd = &cobra.Command{ - Use: "crawl [uri]", + Use: "crawl [uri]", + Example: ` magellan crawl https://bmc.example.com + magellan crawl https://bmc.example.com -i -u username -p password`, Short: "Crawl a single BMC for inventory information", - Long: "Crawl a single BMC for inventory information with URI. This command does NOT scan subnets nor store scan information\n" + - "in cache after completion. To do so, use the 'collect' command instead\n\n" + - "Examples:\n" + - " magellan crawl https://bmc.example.com\n" + - " magellan crawl https://bmc.example.com -i -u username -p password", + Long: "Crawl a single BMC for inventory information with URI.\n\n NOTE: This command does not scan subnets, store scan information in cache, nor make a request to a specified host. It is used only to retrieve inventory data directly. Otherwise, use 'scan' and 'collect' instead.", Args: func(cmd *cobra.Command, args []string) error { // Validate that the only argument is a valid URI var err error @@ -82,8 +80,6 @@ func init() { CrawlCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Ignore SSL errors") CrawlCmd.Flags().StringVarP(&secretsFile, "file", "f", "nodes.json", "set the secrets file with BMC credentials") - checkBindFlagError(viper.BindPFlag("crawl.username", CrawlCmd.Flags().Lookup("username"))) - checkBindFlagError(viper.BindPFlag("crawl.password", CrawlCmd.Flags().Lookup("password"))) checkBindFlagError(viper.BindPFlag("crawl.insecure", CrawlCmd.Flags().Lookup("insecure"))) rootCmd.AddCommand(CrawlCmd) diff --git a/cmd/list.go b/cmd/list.go index 9fbc361..04b6d0a 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -20,14 +20,15 @@ var ( // 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", + Use: "list", + Example: ` magellan list + magellan list --cache ./assets.db + magellan list --cache-info + `, Args: cobra.ExactArgs(0), 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 --cache ./assets.db", + "See the 'scan' command on how to perform a scan.", Run: func(cmd *cobra.Command, args []string) { // check if we just want to show cache-related info and exit if showCache { diff --git a/cmd/root.go b/cmd/root.go index ae43a53..2fde1eb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -52,7 +52,7 @@ var ( var rootCmd = &cobra.Command{ Use: "magellan", Short: "Redfish-based BMC discovery tool", - Long: "", + Long: "Redfish-based BMC discovery tool with dynamic discovery features.", Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { err := cmd.Help() @@ -75,8 +75,8 @@ func Execute() { func init() { currentUser, _ = user.Current() cobra.OnInitialize(InitializeConfig) - rootCmd.PersistentFlags().IntVar(&concurrency, "concurrency", -1, "Set the number of concurrent processes") - rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 5, "Set the timeout for requests") + rootCmd.PersistentFlags().IntVarP(&concurrency, "concurrency", "j", -1, "Set the number of concurrent processes") + rootCmd.PersistentFlags().IntVarP(&timeout, "timeout", "t", 5, "Set the timeout for requests") rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Set the config file path") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Set to enable/disable verbose output") rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Set to enable/disable debug messages") @@ -94,7 +94,7 @@ func init() { func checkBindFlagError(err error) { if err != nil { - log.Error().Err(err).Msg("failed to bind flag") + log.Error().Err(err).Msg("failed to bind cobra/viper flag") } } diff --git a/cmd/scan.go b/cmd/scan.go index 76955c0..5bdbcff 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -33,7 +33,28 @@ var ( // See the `ScanForAssets()` function in 'internal/scan.go' for details // related to the implementation. var ScanCmd = &cobra.Command{ - Use: "scan urls...", + Use: "scan urls...", + Example: ` + // assumes host https://10.0.0.101:443 + magellan scan 10.0.0.101 + + // assumes subnet using HTTPS and port 443 except for specified host + magellan scan http://10.0.0.101:80 https://user:password@10.0.0.102:443 http://172.16.0.105:8080 --subnet 172.16.0.0/24 + + // assumes hosts http://10.0.0.101:8080 and http://10.0.0.102:8080 + magellan scan 10.0.0.101 10.0.0.102 https://172.16.0.10:443 --port 8080 --protocol tcp + + // assumes subnet using default unspecified subnet-masks + magellan scan --subnet 10.0.0.0 + + // assumes subnet using HTTPS and port 443 with specified CIDR + magellan scan --subnet 10.0.0.0/16 + + // assumes subnet using HTTP and port 5000 similar to 192.168.0.0/16 + magellan scan --subnet 192.168.0.0 --protocol tcp --scheme https --port 5000 --subnet-mask 255.255.0.0 + + // assumes subnet without CIDR has a subnet-mask of 255.255.0.0 + magellan scan --subnet 10.0.0.0/24 --subnet 172.16.0.0 --subnet-mask 255.255.0.0 --cache ./assets.db`, Short: "Scan to discover BMC nodes on a network", Long: "Perform a net scan by attempting to connect to each host and port specified and getting a response.\n" + "Each host is passed *with a full URL* including the protocol and port. Additional subnets can be added\n" + @@ -46,22 +67,7 @@ var ScanCmd = &cobra.Command{ "'--protocol' flag.\n\n" + "If the '--disable-probe` flag is used, the tool will not send another request to probe for available.\n" + "Redfish services. This is not recommended, since the extra request makes the scan a bit more reliable\n" + - "for determining which hosts to collect inventory data.\n\n" + - "Examples:\n" + - // assumes host https://10.0.0.101:443 - " magellan scan 10.0.0.101\n" + - // assumes subnet using HTTPS and port 443 except for specified host - " magellan scan http://10.0.0.101:80 https://user:password@10.0.0.102:443 http://172.16.0.105:8080 --subnet 172.16.0.0/24\n" + - // assumes hosts http://10.0.0.101:8080 and http://10.0.0.102:8080 - " magellan scan 10.0.0.101 10.0.0.102 https://172.16.0.10:443 --port 8080 --protocol tcp\n" + - // assumes subnet using default unspecified subnet-masks - " magellan scan --subnet 10.0.0.0\n" + - // assumes subnet using HTTPS and port 443 with specified CIDR - " magellan scan --subnet 10.0.0.0/16\n" + - // assumes subnet using HTTP and port 5000 similar to 192.168.0.0/16 - " magellan scan --subnet 192.168.0.0 --protocol tcp --scheme https --port 5000 --subnet-mask 255.255.0.0\n" + - // assumes subnet without CIDR has a subnet-mask of 255.255.0.0 - " magellan scan --subnet 10.0.0.0/24 --subnet 172.16.0.0 --subnet-mask 255.255.0.0 --cache ./assets.db\n", + "for determining which hosts to collect inventory data.\n\n", Run: func(cmd *cobra.Command, args []string) { // add default ports for hosts if none are specified with flag if len(ports) == 0 { diff --git a/cmd/secrets.go b/cmd/secrets.go index cb6a191..9d18d2e 100644 --- a/cmd/secrets.go +++ b/cmd/secrets.go @@ -20,17 +20,21 @@ var ( ) var secretsCmd = &cobra.Command{ - Use: "secrets", + Use: "secrets", + Example: ` + // generate new key and set environment variable + export MASTER_KEY=$(magellan secrets generatekey) + + // store specific BMC node creds for collect and crawl in default secrets store (--file/-f flag not set) + magellan secrets store $bmc_host $bmc_creds + + // retrieve creds from secrets store + magellan secrets retrieve $bmc_host -f nodes.json + + // list creds from specific secrets + magellan secrets list -f nodes.json`, Short: "Manage credentials for BMC nodes", - Long: "Manage credentials for BMC nodes to for querying information through redfish. This requires generating a key and setting the 'MASTER_KEY' environment variable for the secrets store.\n" + - "Examples:\n\n" + - " export MASTER_KEY=$(magellan secrets generatekey)\n" + - // store specific BMC node creds for `collect` and `crawl` in default secrets store (`--file/-f`` flag not set) - " magellan secrets store $bmc_host $bmc_creds" + - // retrieve creds from secrets store - " magellan secrets retrieve $bmc_host -f nodes.json" + - // list creds from specific secrets - " magellan secrets list -f nodes.json", + Long: "Manage credentials for BMC nodes to for querying information through redfish. This requires generating a key and setting the 'MASTER_KEY' environment variable for the secrets store.", Run: func(cmd *cobra.Command, args []string) { // show command help and exit if len(args) < 1 { @@ -60,7 +64,7 @@ var secretsStoreCmd = &cobra.Command{ Short: "Stores the given string value under secretID.", Run: func(cmd *cobra.Command, args []string) { var ( - secretID = args[0] + secretID string = args[0] secretValue string store secrets.SecretStore inputFileBytes []byte @@ -163,7 +167,7 @@ var secretsStoreCmd = &cobra.Command{ func isValidCredsJSON(val string) bool { var ( - valid = !json.Valid([]byte(val)) + valid bool = !json.Valid([]byte(val)) creds map[string]string err error ) @@ -219,8 +223,8 @@ var secretsListCmd = &cobra.Command{ os.Exit(1) } - for key := range secrets { - fmt.Printf("%s\n", key) + for key, value := range secrets { + fmt.Printf("%s: %s\n", key, value) } }, } diff --git a/cmd/update.go b/cmd/update.go index 65e74c5..42461d5 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -12,7 +12,7 @@ import ( var ( host string - firmwareUrl string + firmwareUri string firmwareVersion string component string transferProtocol string @@ -24,12 +24,16 @@ var ( // using Redfish. It also provides a simple way to check the status of // an update in-progress. var updateCmd = &cobra.Command{ - Use: "update hosts...", + Use: "update hosts...", + Example: ` // perform an firmware update + magellan update 172.16.0.108:443 -i -u $bmc_username -p $bmc_password \ + --firmware-url http://172.16.0.200:8005/firmware/bios/image.RBU \ + --component BIOS + + // check update status + magellan update 172.16.0.108:443 -i -u $bmc_username -p $bmc_password --status`, 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 --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", + Long: "Perform an firmware update using Redfish by providing a remote firmware URL and component.", Run: func(cmd *cobra.Command, args []string) { // check that we have at least one host if len(args) <= 0 { @@ -41,9 +45,7 @@ var updateCmd = &cobra.Command{ for _, arg := range args { if showStatus { err := magellan.GetUpdateStatus(&magellan.UpdateParams{ - FirmwarePath: firmwareUrl, - FirmwareVersion: firmwareVersion, - Component: component, + FirmwareURI: firmwareUri, TransferProtocol: transferProtocol, Insecure: Insecure, CollectParams: magellan.CollectParams{ @@ -61,9 +63,7 @@ var updateCmd = &cobra.Command{ // initiate a remote update err := magellan.UpdateFirmwareRemote(&magellan.UpdateParams{ - FirmwarePath: firmwareUrl, - FirmwareVersion: firmwareVersion, - Component: component, + FirmwareURI: firmwareUri, TransferProtocol: strings.ToUpper(transferProtocol), Insecure: Insecure, CollectParams: magellan.CollectParams{ @@ -81,21 +81,15 @@ var updateCmd = &cobra.Command{ } func init() { - updateCmd.Flags().StringVar(&username, "username", "", "Set the BMC user") - updateCmd.Flags().StringVar(&password, "password", "", "Set the BMC password") + updateCmd.Flags().StringVarP(&username, "username", "u", "", "Set the BMC user") + updateCmd.Flags().StringVarP(&password, "password", "p", "", "Set the BMC password") updateCmd.Flags().StringVar(&transferProtocol, "scheme", "https", "Set the transfer protocol") - updateCmd.Flags().StringVar(&firmwareUrl, "firmware-url", "", "Set the path to the firmware") - updateCmd.Flags().StringVar(&firmwareVersion, "firmware-version", "", "Set the version of firmware to be installed") - updateCmd.Flags().StringVar(&component, "component", "", "Set the component to upgrade (BMC|BIOS)") + updateCmd.Flags().StringVar(&firmwareUri, "firmware-uri", "", "Set the URI to retrieve the firmware") updateCmd.Flags().BoolVar(&showStatus, "status", false, "Get the status of the update") - updateCmd.Flags().BoolVar(&Insecure, "insecure", false, "Allow insecure connections to the server") + updateCmd.Flags().BoolVarP(&Insecure, "insecure", "i", 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"))) checkBindFlagError(viper.BindPFlag("update.scheme", updateCmd.Flags().Lookup("scheme"))) - checkBindFlagError(viper.BindPFlag("update.firmware-url", updateCmd.Flags().Lookup("firmware-url"))) - checkBindFlagError(viper.BindPFlag("update.firmware-version", updateCmd.Flags().Lookup("firmware-version"))) - checkBindFlagError(viper.BindPFlag("update.component", updateCmd.Flags().Lookup("component"))) + checkBindFlagError(viper.BindPFlag("update.firmware-uri", updateCmd.Flags().Lookup("firmware-uri"))) checkBindFlagError(viper.BindPFlag("update.status", updateCmd.Flags().Lookup("status"))) checkBindFlagError(viper.BindPFlag("update.insecure", updateCmd.Flags().Lookup("insecure"))) diff --git a/pkg/update.go b/pkg/update.go index 5f3e3e9..0af0a9e 100644 --- a/pkg/update.go +++ b/pkg/update.go @@ -10,9 +10,7 @@ import ( type UpdateParams struct { CollectParams - FirmwarePath string - FirmwareVersion string - Component string + FirmwareURI string TransferProtocol string Insecure bool } @@ -51,7 +49,7 @@ func UpdateFirmwareRemote(q *UpdateParams) error { // Build the update request payload req := redfish.SimpleUpdateParameters{ - ImageURI: q.FirmwarePath, + ImageURI: q.FirmwareURI, TransferProtocol: redfish.TransferProtocolType(q.TransferProtocol), }