refactor: improvements to CLI and update pkg

This commit is contained in:
David Allen 2025-03-28 13:12:38 -06:00
parent 2fca8f9166
commit c950532e88
Signed by: towk
GPG key ID: 793B2924A49B3A3F
8 changed files with 86 additions and 87 deletions

View file

@ -20,16 +20,18 @@ import (
// on a subnet. // on a subnet.
var CollectCmd = &cobra.Command{ 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", 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" + 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.",
"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",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// get probe states stored in db from scan // get probe states stored in db from scan
scannedResults, err := sqlite.GetScannedAssets(cachePath) scannedResults, err := sqlite.GetScannedAssets(cachePath)
@ -110,8 +112,6 @@ func init() {
// bind flags to config properties // bind flags to config properties
checkBindFlagError(viper.BindPFlag("collect.host", CollectCmd.Flags().Lookup("host"))) 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.scheme", CollectCmd.Flags().Lookup("scheme")))
checkBindFlagError(viper.BindPFlag("collect.protocol", CollectCmd.Flags().Lookup("protocol"))) checkBindFlagError(viper.BindPFlag("collect.protocol", CollectCmd.Flags().Lookup("protocol")))
checkBindFlagError(viper.BindPFlag("collect.output", CollectCmd.Flags().Lookup("output"))) checkBindFlagError(viper.BindPFlag("collect.output", CollectCmd.Flags().Lookup("output")))

View file

@ -18,12 +18,10 @@ import (
// not require a scan to be performed beforehand. // not require a scan to be performed beforehand.
var CrawlCmd = &cobra.Command{ 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", 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" + 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.",
"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",
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
// Validate that the only argument is a valid URI // Validate that the only argument is a valid URI
var err error var err error
@ -82,8 +80,6 @@ func init() {
CrawlCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Ignore SSL errors") 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") 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"))) checkBindFlagError(viper.BindPFlag("crawl.insecure", CrawlCmd.Flags().Lookup("insecure")))
rootCmd.AddCommand(CrawlCmd) rootCmd.AddCommand(CrawlCmd)

View file

@ -21,13 +21,14 @@ var (
// is what is consumed by the `collect` command with the --cache flag. // is what is consumed by the `collect` command with the --cache flag.
var ListCmd = &cobra.Command{ var ListCmd = &cobra.Command{
Use: "list", Use: "list",
Example: ` magellan list
magellan list --cache ./assets.db
magellan list --cache-info
`,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
Short: "List information stored in cache from a 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" + 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" + "See the 'scan' command on how to perform a scan.",
"Examples:\n" +
" magellan list\n" +
" magellan list --cache ./assets.db",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// check if we just want to show cache-related info and exit // check if we just want to show cache-related info and exit
if showCache { if showCache {

View file

@ -52,7 +52,7 @@ var (
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "magellan", Use: "magellan",
Short: "Redfish-based BMC discovery tool", Short: "Redfish-based BMC discovery tool",
Long: "", Long: "Redfish-based BMC discovery tool with dynamic discovery features.",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 { if len(args) == 0 {
err := cmd.Help() err := cmd.Help()
@ -75,8 +75,8 @@ func Execute() {
func init() { func init() {
currentUser, _ = user.Current() currentUser, _ = user.Current()
cobra.OnInitialize(InitializeConfig) cobra.OnInitialize(InitializeConfig)
rootCmd.PersistentFlags().IntVar(&concurrency, "concurrency", -1, "Set the number of concurrent processes") rootCmd.PersistentFlags().IntVarP(&concurrency, "concurrency", "j", -1, "Set the number of concurrent processes")
rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 5, "Set the timeout for requests") 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().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(&verbose, "verbose", "v", false, "Set to enable/disable verbose output")
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Set to enable/disable debug messages") rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Set to enable/disable debug messages")
@ -94,7 +94,7 @@ func init() {
func checkBindFlagError(err error) { func checkBindFlagError(err error) {
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to bind flag") log.Error().Err(err).Msg("failed to bind cobra/viper flag")
} }
} }

View file

@ -34,6 +34,27 @@ var (
// related to the implementation. // related to the implementation.
var ScanCmd = &cobra.Command{ 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", 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" + 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" + "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" + "'--protocol' flag.\n\n" +
"If the '--disable-probe` flag is used, the tool will not send another request to probe for available.\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" + "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" + "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",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// add default ports for hosts if none are specified with flag // add default ports for hosts if none are specified with flag
if len(ports) == 0 { if len(ports) == 0 {

View file

@ -21,16 +21,20 @@ var (
var secretsCmd = &cobra.Command{ var secretsCmd = &cobra.Command{
Use: "secrets", Use: "secrets",
Short: "Manage credentials for BMC nodes", Example: `
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" + // generate new key and set environment variable
"Examples:\n\n" + export MASTER_KEY=$(magellan secrets generatekey)
" 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) // 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" + magellan secrets store $bmc_host $bmc_creds
// retrieve creds from secrets store // retrieve creds from secrets store
" magellan secrets retrieve $bmc_host -f nodes.json" + magellan secrets retrieve $bmc_host -f nodes.json
// list creds from specific secrets // list creds from specific secrets
" magellan secrets list -f nodes.json", 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.",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// show command help and exit // show command help and exit
if len(args) < 1 { if len(args) < 1 {
@ -60,7 +64,7 @@ var secretsStoreCmd = &cobra.Command{
Short: "Stores the given string value under secretID.", Short: "Stores the given string value under secretID.",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var ( var (
secretID = args[0] secretID string = args[0]
secretValue string secretValue string
store secrets.SecretStore store secrets.SecretStore
inputFileBytes []byte inputFileBytes []byte
@ -163,7 +167,7 @@ var secretsStoreCmd = &cobra.Command{
func isValidCredsJSON(val string) bool { func isValidCredsJSON(val string) bool {
var ( var (
valid = !json.Valid([]byte(val)) valid bool = !json.Valid([]byte(val))
creds map[string]string creds map[string]string
err error err error
) )
@ -219,8 +223,8 @@ var secretsListCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
for key := range secrets { for key, value := range secrets {
fmt.Printf("%s\n", key) fmt.Printf("%s: %s\n", key, value)
} }
}, },
} }

View file

@ -12,7 +12,7 @@ import (
var ( var (
host string host string
firmwareUrl string firmwareUri string
firmwareVersion string firmwareVersion string
component string component string
transferProtocol string transferProtocol string
@ -25,11 +25,15 @@ var (
// an update in-progress. // an update in-progress.
var updateCmd = &cobra.Command{ 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", Short: "Update BMC node firmware",
Long: "Perform an firmware update using Redfish by providing a remote firmware URL and component.\n\n" + Long: "Perform an firmware update using Redfish by providing a remote firmware URL and component.",
"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",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// check that we have at least one host // check that we have at least one host
if len(args) <= 0 { if len(args) <= 0 {
@ -41,9 +45,7 @@ var updateCmd = &cobra.Command{
for _, arg := range args { for _, arg := range args {
if showStatus { if showStatus {
err := magellan.GetUpdateStatus(&magellan.UpdateParams{ err := magellan.GetUpdateStatus(&magellan.UpdateParams{
FirmwarePath: firmwareUrl, FirmwareURI: firmwareUri,
FirmwareVersion: firmwareVersion,
Component: component,
TransferProtocol: transferProtocol, TransferProtocol: transferProtocol,
Insecure: Insecure, Insecure: Insecure,
CollectParams: magellan.CollectParams{ CollectParams: magellan.CollectParams{
@ -61,9 +63,7 @@ var updateCmd = &cobra.Command{
// initiate a remote update // initiate a remote update
err := magellan.UpdateFirmwareRemote(&magellan.UpdateParams{ err := magellan.UpdateFirmwareRemote(&magellan.UpdateParams{
FirmwarePath: firmwareUrl, FirmwareURI: firmwareUri,
FirmwareVersion: firmwareVersion,
Component: component,
TransferProtocol: strings.ToUpper(transferProtocol), TransferProtocol: strings.ToUpper(transferProtocol),
Insecure: Insecure, Insecure: Insecure,
CollectParams: magellan.CollectParams{ CollectParams: magellan.CollectParams{
@ -81,21 +81,15 @@ var updateCmd = &cobra.Command{
} }
func init() { func init() {
updateCmd.Flags().StringVar(&username, "username", "", "Set the BMC user") updateCmd.Flags().StringVarP(&username, "username", "u", "", "Set the BMC user")
updateCmd.Flags().StringVar(&password, "password", "", "Set the BMC password") updateCmd.Flags().StringVarP(&password, "password", "p", "", "Set the BMC password")
updateCmd.Flags().StringVar(&transferProtocol, "scheme", "https", "Set the transfer protocol") 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(&firmwareUri, "firmware-uri", "", "Set the URI to retrieve 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().BoolVar(&showStatus, "status", false, "Get the status of the update") 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.scheme", updateCmd.Flags().Lookup("scheme")))
checkBindFlagError(viper.BindPFlag("update.firmware-url", updateCmd.Flags().Lookup("firmware-url"))) checkBindFlagError(viper.BindPFlag("update.firmware-uri", updateCmd.Flags().Lookup("firmware-uri")))
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.status", updateCmd.Flags().Lookup("status")))
checkBindFlagError(viper.BindPFlag("update.insecure", updateCmd.Flags().Lookup("insecure"))) checkBindFlagError(viper.BindPFlag("update.insecure", updateCmd.Flags().Lookup("insecure")))

View file

@ -10,9 +10,7 @@ import (
type UpdateParams struct { type UpdateParams struct {
CollectParams CollectParams
FirmwarePath string FirmwareURI string
FirmwareVersion string
Component string
TransferProtocol string TransferProtocol string
Insecure bool Insecure bool
} }
@ -51,7 +49,7 @@ func UpdateFirmwareRemote(q *UpdateParams) error {
// Build the update request payload // Build the update request payload
req := redfish.SimpleUpdateParameters{ req := redfish.SimpleUpdateParameters{
ImageURI: q.FirmwarePath, ImageURI: q.FirmwareURI,
TransferProtocol: redfish.TransferProtocolType(q.TransferProtocol), TransferProtocol: redfish.TransferProtocolType(q.TransferProtocol),
} }