mirror of
https://github.com/davidallendj/magellan.git
synced 2025-12-20 03:27:03 -07:00
Split the collect Command For Customization (#93)
* feat: initial implementation of command split * feat: update collect and new send cmd * chore: cleanup unused code * chore: refactored getting username * chore: more refactoring and cleanup * feat: update send cmd implementation * chore: changed/updated example config * chore: made cmd more consistent and added formatting * refactor: removed --host flag from scan * chore: cleaned up and fixed issue with client * chore: cleaned up CLI flags in collect cmd * feat: updated crawl to include managers and output YAML optionally * refactor: updated and improved send implementation * refactor: minor improvements * refactor: added util func to check for empty slices * fix: issue with reading from stdin * refactor: added scheme trimming function for URIs * refactor: changed host arg back to positional * refactor: removed unused vars and added --output-dir flag * fix: make -f for secrets persistent * refactor: removed --host flag and request in collect * refactor: changed --output flag to --output-file * fix: updated flags for collect * fix: typo in crawler error * fix: dir being created when outputDir not set * fix: reading stdin and data args * fix: made output using -v and -o consistent * readme: added info about command split * updated changelog adding missing version entries * chore: updated example to use host as positional arg * fix: issue with reading --data arg * fix: remove unused import from collect pkg Signed-off-by: Devon Bautista <devonb@lanl.gov> --------- Signed-off-by: David Allen <16520934+davidallendj@users.noreply.github.com> Signed-off-by: Devon Bautista <devonb@lanl.gov> Co-authored-by: Devon Bautista <devonb@lanl.gov>
This commit is contained in:
parent
fba4a89a0e
commit
04e1fb26c9
19 changed files with 736 additions and 223 deletions
|
|
@ -2,8 +2,6 @@ package cmd
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/user"
|
||||
|
||||
"github.com/OpenCHAMI/magellan/internal/cache/sqlite"
|
||||
urlx "github.com/OpenCHAMI/magellan/internal/url"
|
||||
|
|
@ -17,6 +15,8 @@ import (
|
|||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var collectOutputFormat string
|
||||
|
||||
// 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.
|
||||
|
|
@ -122,6 +122,8 @@ var CollectCmd = &cobra.Command{
|
|||
Verbose: verbose,
|
||||
CaCertPath: cacertPath,
|
||||
OutputPath: outputPath,
|
||||
OutputDir: outputDir,
|
||||
Format: collectOutputFormat,
|
||||
ForceUpdate: forceUpdate,
|
||||
AccessToken: accessToken,
|
||||
SecretStore: store,
|
||||
|
|
@ -140,22 +142,24 @@ var CollectCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func init() {
|
||||
currentUser, _ = user.Current()
|
||||
CollectCmd.Flags().StringVar(&host, "host", "", "Set the URI to the SMD root endpoint")
|
||||
CollectCmd.Flags().StringVarP(&username, "username", "u", "", "Set the master BMC username")
|
||||
CollectCmd.Flags().StringVarP(&password, "password", "p", "", "Set the master BMC password")
|
||||
CollectCmd.Flags().StringVar(&secretsFile, "secrets-file", "", "Set path to the node secrets file")
|
||||
CollectCmd.Flags().StringVar(&scheme, "scheme", "https", "Set the default scheme used to query when not included in URI")
|
||||
CollectCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the protocol used to query")
|
||||
CollectCmd.Flags().StringVarP(&outputPath, "output", "o", fmt.Sprintf("/tmp/%smagellan/inventory/", currentUser.Username+"/"), "Set the path to store collection data")
|
||||
CollectCmd.Flags().StringVarP(&outputPath, "output-file", "o", "", "Set the path to store collection data using HIVE partitioning")
|
||||
CollectCmd.Flags().StringVarP(&outputDir, "output-dir", "O", "", "Set the path to store collection data using HIVE partitioning")
|
||||
CollectCmd.Flags().BoolVar(&forceUpdate, "force-update", false, "Set flag to force update data sent to SMD")
|
||||
CollectCmd.Flags().StringVar(&cacertPath, "cacert", "", "Set the path to CA cert file. (defaults to system CAs when blank)")
|
||||
CollectCmd.Flags().StringVar(&cacertPath, "cacert", "", "Set the path to CA cert file (defaults to system CAs when blank)")
|
||||
CollectCmd.Flags().StringVarP(&collectOutputFormat, "format", "F", FORMAT_JSON, "Set the output format (json|yaml)")
|
||||
|
||||
CollectCmd.MarkFlagsMutuallyExclusive("output-file", "output-dir")
|
||||
|
||||
// bind flags to config properties
|
||||
checkBindFlagError(viper.BindPFlag("collect.host", CollectCmd.Flags().Lookup("host")))
|
||||
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")))
|
||||
checkBindFlagError(viper.BindPFlag("collect.output-file", CollectCmd.Flags().Lookup("output-file")))
|
||||
checkBindFlagError(viper.BindPFlag("collect.output-dir", CollectCmd.Flags().Lookup("output-dir")))
|
||||
checkBindFlagError(viper.BindPFlag("collect.force-update", CollectCmd.Flags().Lookup("force-update")))
|
||||
checkBindFlagError(viper.BindPFlag("collect.cacert", CollectCmd.Flags().Lookup("cacert")))
|
||||
checkBindFlagError(viper.BindPFlags(CollectCmd.Flags()))
|
||||
|
|
|
|||
71
cmd/crawl.go
71
cmd/crawl.go
|
|
@ -3,8 +3,10 @@ package cmd
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
urlx "github.com/OpenCHAMI/magellan/internal/url"
|
||||
"github.com/OpenCHAMI/magellan/pkg/bmc"
|
||||
|
|
@ -14,6 +16,8 @@ import (
|
|||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var crawlOutputFormat string
|
||||
|
||||
// The `crawl` command walks a collection of Redfish endpoints to collect
|
||||
// specfic inventory detail. This command only expects host names and does
|
||||
// not require a scan to be performed beforehand.
|
||||
|
|
@ -37,9 +41,10 @@ var CrawlCmd = &cobra.Command{
|
|||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
uri = args[0]
|
||||
store secrets.SecretStore
|
||||
err error
|
||||
uri = args[0]
|
||||
store secrets.SecretStore
|
||||
output []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if username != "" && password != "" {
|
||||
|
|
@ -76,24 +81,53 @@ var CrawlCmd = &cobra.Command{
|
|||
store = &nodeCreds
|
||||
}
|
||||
|
||||
systems, err := crawler.CrawlBMCForSystems(crawler.CrawlerConfig{
|
||||
URI: uri,
|
||||
CredentialStore: store,
|
||||
Insecure: insecure,
|
||||
UseDefault: true,
|
||||
})
|
||||
var (
|
||||
systems []crawler.InventoryDetail
|
||||
managers []crawler.Manager
|
||||
config = crawler.CrawlerConfig{
|
||||
URI: uri,
|
||||
CredentialStore: store,
|
||||
Insecure: insecure,
|
||||
UseDefault: true,
|
||||
}
|
||||
)
|
||||
|
||||
systems, err = crawler.CrawlBMCForSystems(config)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to crawl BMC")
|
||||
log.Error().Err(err).Msg("failed to crawl BMC for systems")
|
||||
}
|
||||
// Marshal the inventory details to JSON
|
||||
jsonData, err := json.MarshalIndent(systems, "", " ")
|
||||
managers, err = crawler.CrawlBMCForManagers(config)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal JSON")
|
||||
return
|
||||
log.Error().Err(err).Msg("failed to crawl BMC for managers")
|
||||
}
|
||||
|
||||
// Print the pretty JSON
|
||||
fmt.Println(string(jsonData))
|
||||
data := map[string]any{
|
||||
"Systems": systems,
|
||||
"Managers": managers,
|
||||
}
|
||||
|
||||
switch crawlOutputFormat {
|
||||
case FORMAT_JSON:
|
||||
// Marshal the inventory details to JSON
|
||||
output, err = json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal JSON")
|
||||
return
|
||||
}
|
||||
case FORMAT_YAML:
|
||||
// Marshal the inventory details to JSON
|
||||
output, err = yaml.Marshal(data)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal JSON")
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Error().Str("hint", "Try setting --format/-F to 'json' or 'yaml'").Msg("unrecognized format")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Print the pretty JSON or YAML
|
||||
fmt.Println(string(output))
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +135,11 @@ func init() {
|
|||
CrawlCmd.Flags().StringVarP(&username, "username", "u", "", "Set the username for the BMC")
|
||||
CrawlCmd.Flags().StringVarP(&password, "password", "p", "", "Set the password for the BMC")
|
||||
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, "secrets-file", "f", "secrets.json", "Set path to the node secrets file")
|
||||
CrawlCmd.Flags().StringVarP(&crawlOutputFormat, "format", "F", FORMAT_JSON, "Set the output format (json|yaml)")
|
||||
|
||||
checkBindFlagError(viper.BindPFlag("crawl.insecure", CrawlCmd.Flags().Lookup("insecure")))
|
||||
checkBindFlagError(viper.BindPFlag("crawl.insecure", CrawlCmd.Flags().Lookup("insecure")))
|
||||
checkBindFlagError(viper.BindPFlag("crawl.insecure", CrawlCmd.Flags().Lookup("insecure")))
|
||||
|
||||
rootCmd.AddCommand(CrawlCmd)
|
||||
|
|
|
|||
24
cmd/list.go
24
cmd/list.go
|
|
@ -3,17 +3,20 @@ package cmd
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/OpenCHAMI/magellan/internal/cache/sqlite"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
showCache bool
|
||||
showCache bool
|
||||
listOutputFormat string
|
||||
)
|
||||
|
||||
// The `list` command provides an easy way to show what was found
|
||||
|
|
@ -41,23 +44,32 @@ var ListCmd = &cobra.Command{
|
|||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get scanned assets")
|
||||
}
|
||||
format = strings.ToLower(format)
|
||||
if format == "json" {
|
||||
switch strings.ToLower(listOutputFormat) {
|
||||
case FORMAT_JSON:
|
||||
b, err := json.Marshal(scannedResults)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed to unmarshal scanned results")
|
||||
log.Error().Err(err).Msgf("failed to unmarshal cached data to JSON")
|
||||
}
|
||||
fmt.Printf("%s\n", string(b))
|
||||
} else {
|
||||
case FORMAT_YAML:
|
||||
b, err := yaml.Marshal(scannedResults)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed to unmarshal cached data to YAML")
|
||||
}
|
||||
fmt.Printf("%s\n", string(b))
|
||||
case FORMAT_LIST:
|
||||
for _, r := range scannedResults {
|
||||
fmt.Printf("%s:%d (%s) @%s\n", r.Host, r.Port, r.Protocol, r.Timestamp.Format(time.UnixDate))
|
||||
}
|
||||
default:
|
||||
log.Error().Msg("unrecognized format")
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ListCmd.Flags().StringVar(&format, "format", "", "Set the output format (json|default)")
|
||||
ListCmd.Flags().StringVarP(&listOutputFormat, "format", "F", FORMAT_LIST, "Set the output format (json|yaml|table)")
|
||||
ListCmd.Flags().BoolVar(&showCache, "cache-info", false, "Show cache information and exit")
|
||||
rootCmd.AddCommand(ListCmd)
|
||||
}
|
||||
|
|
|
|||
23
cmd/root.go
23
cmd/root.go
|
|
@ -18,28 +18,33 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
magellan "github.com/OpenCHAMI/magellan/internal"
|
||||
"github.com/OpenCHAMI/magellan/internal/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
FORMAT_LIST = "list"
|
||||
FORMAT_JSON = "json"
|
||||
FORMAT_YAML = "yaml"
|
||||
)
|
||||
|
||||
// CLI arguments as variables to not fiddle with error-prone strings
|
||||
var (
|
||||
currentUser *user.User
|
||||
accessToken string
|
||||
format string
|
||||
timeout int
|
||||
concurrency int
|
||||
ports []int
|
||||
hosts []string
|
||||
protocol string
|
||||
cacertPath string
|
||||
username string
|
||||
password string
|
||||
cachePath string
|
||||
outputPath string
|
||||
outputDir string
|
||||
configPath string
|
||||
verbose bool
|
||||
debug bool
|
||||
|
|
@ -73,15 +78,14 @@ func Execute() {
|
|||
}
|
||||
|
||||
func init() {
|
||||
currentUser, _ = user.Current()
|
||||
cobra.OnInitialize(InitializeConfig)
|
||||
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().IntVarP(&timeout, "timeout", "t", 5, "Set the timeout for requests in seconds")
|
||||
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")
|
||||
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Set to enable/disable debug messages")
|
||||
rootCmd.PersistentFlags().StringVar(&accessToken, "access-token", "", "Set the access token")
|
||||
rootCmd.PersistentFlags().StringVar(&cachePath, "cache", fmt.Sprintf("/tmp/%s/magellan/assets.db", currentUser.Username), "Set the scanning result cache path")
|
||||
rootCmd.PersistentFlags().StringVar(&cachePath, "cache", fmt.Sprintf("/tmp/%s/magellan/assets.db", util.GetCurrentUsername()), "Set the scanning result cache path")
|
||||
|
||||
// bind viper config flags with cobra
|
||||
checkBindFlagError(viper.BindPFlag("concurrency", rootCmd.PersistentFlags().Lookup("concurrency")))
|
||||
|
|
@ -117,13 +121,12 @@ func InitializeConfig() {
|
|||
// TODO: This function should probably be moved to 'internal/config.go'
|
||||
// instead of in this file.
|
||||
func SetDefaults() {
|
||||
currentUser, _ = user.Current()
|
||||
viper.SetDefault("threads", 1)
|
||||
viper.SetDefault("timeout", 5)
|
||||
viper.SetDefault("config", "")
|
||||
viper.SetDefault("verbose", false)
|
||||
viper.SetDefault("debug", false)
|
||||
viper.SetDefault("cache", fmt.Sprintf("/tmp/%s/magellan/assets.db", currentUser.Username))
|
||||
viper.SetDefault("cache", fmt.Sprintf("/tmp/%s/magellan/assets.db", util.GetCurrentUsername()))
|
||||
viper.SetDefault("scan.hosts", []string{})
|
||||
viper.SetDefault("scan.ports", []int{})
|
||||
viper.SetDefault("scan.subnets", []string{})
|
||||
|
|
|
|||
13
cmd/scan.go
13
cmd/scan.go
|
|
@ -79,18 +79,8 @@ var ScanCmd = &cobra.Command{
|
|||
|
||||
// format and combine flag and positional args
|
||||
targetHosts = append(targetHosts, urlx.FormatHosts(args, ports, scheme, verbose)...)
|
||||
targetHosts = append(targetHosts, urlx.FormatHosts(hosts, ports, scheme, verbose)...)
|
||||
|
||||
// add more hosts specified with `--subnet` flag
|
||||
if debug {
|
||||
log.Debug().Msg("adding hosts from subnets")
|
||||
}
|
||||
for _, subnet := range subnets {
|
||||
// subnet string is empty so nothing to do here
|
||||
if subnet == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// generate a slice of all hosts to scan from subnets
|
||||
subnetHosts := magellan.GenerateHostsWithSubnet(subnet, &subnetMask, ports, scheme)
|
||||
targetHosts = append(targetHosts, subnetHosts...)
|
||||
|
|
@ -180,8 +170,6 @@ var ScanCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func init() {
|
||||
// scanCmd.Flags().StringSliceVar(&hosts, "host", []string{}, "set additional hosts to scan")
|
||||
ScanCmd.Flags().StringSliceVar(&hosts, "host", nil, "Add individual hosts to scan. (example: https://my.bmc.com:5000; same as using positional args)")
|
||||
ScanCmd.Flags().IntSliceVar(&ports, "port", nil, "Adds additional ports to scan for each host with unspecified ports.")
|
||||
ScanCmd.Flags().StringVar(&scheme, "scheme", "https", "Set the default scheme to use if not specified in host URI. (default is 'https')")
|
||||
ScanCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the default protocol to use in scan. (default is 'tcp')")
|
||||
|
|
@ -190,7 +178,6 @@ func init() {
|
|||
ScanCmd.Flags().BoolVar(&disableProbing, "disable-probing", false, "Disable probing found assets for Redfish service(s) running on BMC nodes")
|
||||
ScanCmd.Flags().BoolVar(&disableCache, "disable-cache", false, "Disable saving found assets to a cache database specified with 'cache' flag")
|
||||
|
||||
checkBindFlagError(viper.BindPFlag("scan.hosts", ScanCmd.Flags().Lookup("host")))
|
||||
checkBindFlagError(viper.BindPFlag("scan.ports", ScanCmd.Flags().Lookup("port")))
|
||||
checkBindFlagError(viper.BindPFlag("scan.scheme", ScanCmd.Flags().Lookup("scheme")))
|
||||
checkBindFlagError(viper.BindPFlag("scan.protocol", ScanCmd.Flags().Lookup("protocol")))
|
||||
|
|
|
|||
|
|
@ -256,9 +256,9 @@ var secretsRemoveCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func init() {
|
||||
secretsCmd.Flags().StringVarP(&secretsFile, "file", "f", "nodes.json", "set the secrets file with BMC credentials")
|
||||
secretsStoreCmd.Flags().StringVarP(&secretsStoreFormat, "format", "F", "basic", "set the input format for the secrets file (basic|json|base64)")
|
||||
secretsStoreCmd.Flags().StringVarP(&secretsStoreInputFile, "input-file", "i", "", "set the file to read as input")
|
||||
secretsCmd.PersistentFlags().StringVarP(&secretsFile, "file", "f", "secrets.json", "Set the secrets file with BMC credentials.")
|
||||
secretsStoreCmd.Flags().StringVarP(&secretsStoreFormat, "format", "F", "basic", "Set the input format for the secrets file (basic|json|base64).")
|
||||
secretsStoreCmd.Flags().StringVarP(&secretsStoreInputFile, "input-file", "i", "", "Set the file to read as input.")
|
||||
|
||||
secretsCmd.AddCommand(secretsGenerateKeyCmd)
|
||||
secretsCmd.AddCommand(secretsStoreCmd)
|
||||
|
|
@ -268,7 +268,7 @@ func init() {
|
|||
|
||||
rootCmd.AddCommand(secretsCmd)
|
||||
|
||||
checkBindFlagError(viper.BindPFlags(secretsCmd.Flags()))
|
||||
checkBindFlagError(viper.BindPFlags(secretsCmd.PersistentFlags()))
|
||||
checkBindFlagError(viper.BindPFlags(secretsGenerateKeyCmd.Flags()))
|
||||
checkBindFlagError(viper.BindPFlags(secretsStoreCmd.Flags()))
|
||||
checkBindFlagError(viper.BindPFlags(secretsGenerateKeyCmd.Flags()))
|
||||
|
|
|
|||
283
cmd/send.go
Normal file
283
cmd/send.go
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
urlx "github.com/OpenCHAMI/magellan/internal/url"
|
||||
"github.com/OpenCHAMI/magellan/pkg/auth"
|
||||
"github.com/OpenCHAMI/magellan/pkg/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
sendInputFormat string
|
||||
sendDataArgs []string
|
||||
)
|
||||
|
||||
var sendCmd = &cobra.Command{
|
||||
Use: "send [data]",
|
||||
Example: ` // minimal working example
|
||||
magellan send -d @inventory.json https://smd.openchami.cluster
|
||||
|
||||
// send data from multiple files (must specify -f/--format if not JSON)
|
||||
magellan send -d @cluster-1.json -d @cluster-2.json https://smd.openchami.cluster
|
||||
magellan send -d '{...}' -d @cluster-1.json https://proxy.example.com
|
||||
|
||||
// send data to remote host by piping output of collect directly
|
||||
magellan collect -v -F yaml | magellan send -d @inventory.yaml -F yaml https://smd.openchami.cluster`,
|
||||
Short: "Send collected node information to specified host.",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// try to load access token either from env var, file, or config if var not set
|
||||
if accessToken == "" {
|
||||
var err error
|
||||
accessToken, err = auth.LoadAccessToken(tokenPath)
|
||||
if err != nil && verbose {
|
||||
log.Warn().Err(err).Msgf("could not load access token")
|
||||
} else if debug && accessToken != "" {
|
||||
log.Debug().Str("access_token", accessToken).Msg("using access token")
|
||||
}
|
||||
}
|
||||
|
||||
// try and load cert if argument is passed for client
|
||||
var smdClient = client.NewSmdClient()
|
||||
if cacertPath != "" {
|
||||
log.Debug().Str("path", cacertPath).Msg("using provided certificate path")
|
||||
err := client.LoadCertificateFromPath(smdClient, cacertPath)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("could not load certificate")
|
||||
}
|
||||
}
|
||||
|
||||
// make one request be host positional argument (restricted to 1 for now)
|
||||
var inputData []map[string]any
|
||||
temp := append(handleArgs(args), processDataArgs(sendDataArgs)...)
|
||||
for _, data := range temp {
|
||||
if data != nil {
|
||||
inputData = append(inputData, data)
|
||||
}
|
||||
}
|
||||
if len(inputData) == 0 {
|
||||
log.Error().Msg("must include data with standard input or -d/--data flag")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// show the data that was just loaded as input
|
||||
if verbose {
|
||||
output, err := json.Marshal(inputData)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal input data")
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
|
||||
for _, host := range args {
|
||||
var (
|
||||
body []byte
|
||||
err error
|
||||
)
|
||||
|
||||
smdClient.URI = host
|
||||
for _, dataObject := range inputData {
|
||||
// skip on to the next thing if it's does not exist
|
||||
if dataObject == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// create and set headers for request
|
||||
headers := client.HTTPHeader{}
|
||||
headers.Authorization(accessToken)
|
||||
headers.ContentType("application/json")
|
||||
|
||||
host, err = urlx.Sanitize(host)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("host", host).Msg("could not sanitize host")
|
||||
}
|
||||
|
||||
// convert to JSON to send data
|
||||
body, err = json.MarshalIndent(dataObject, "", " ")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal request data")
|
||||
continue
|
||||
}
|
||||
|
||||
err = smdClient.Add(body, headers)
|
||||
if err != nil {
|
||||
// try updating instead
|
||||
if forceUpdate {
|
||||
smdClient.Xname = dataObject["ID"].(string)
|
||||
err = smdClient.Update(body, headers)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed to forcibly update Redfish endpoint with ID %s", smdClient.Xname)
|
||||
}
|
||||
} else {
|
||||
log.Error().Err(err).Msgf("failed to add Redfish endpoint with ID %s", smdClient.Xname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
sendCmd.Flags().StringArrayVarP(&sendDataArgs, "data", "d", []string{}, "Set the data to send to specified host (prepend @ for files)")
|
||||
sendCmd.Flags().StringVarP(&sendInputFormat, "format", "F", FORMAT_JSON, "Set the data input format (json|yaml)")
|
||||
sendCmd.Flags().BoolVarP(&forceUpdate, "force-update", "f", false, "Set flag to force update data sent to SMD")
|
||||
sendCmd.Flags().StringVar(&cacertPath, "cacert", "", "Set the path to CA cert file (defaults to system CAs when blank)")
|
||||
rootCmd.AddCommand(sendCmd)
|
||||
}
|
||||
|
||||
// processDataArgs takes a slice of strings that check for the @ symbol and loads
|
||||
// the contents from the file specified in place (which replaces the path).
|
||||
//
|
||||
// NOTE: The purpose is to make the input arguments uniform for our request. This
|
||||
// function is meant to handle data passed with the `-d/--data` flag and positional
|
||||
// args from the CLI.
|
||||
func processDataArgs(args []string) []map[string]any {
|
||||
// JSON representation
|
||||
type (
|
||||
JSONObject = map[string]any
|
||||
JSONArray = []JSONObject
|
||||
)
|
||||
|
||||
// load data either from file or directly from args
|
||||
var collection = make(JSONArray, len(args))
|
||||
for i, arg := range args {
|
||||
// if arg is empty string, then skip and continue
|
||||
if len(arg) > 0 {
|
||||
// determine if we're reading from file to load contents
|
||||
if strings.HasPrefix(arg, "@") {
|
||||
var (
|
||||
path string = strings.TrimLeft(arg, "@")
|
||||
contents []byte
|
||||
data JSONArray
|
||||
err error
|
||||
)
|
||||
|
||||
contents, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("failed to read file")
|
||||
continue
|
||||
}
|
||||
|
||||
// skip empty files
|
||||
if len(contents) == 0 {
|
||||
log.Warn().Str("path", path).Msg("file is empty")
|
||||
continue
|
||||
}
|
||||
|
||||
// convert/validate JSON input format
|
||||
data, err = parseInput(contents)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("failed to validate input from file")
|
||||
}
|
||||
|
||||
// add loaded data to collection of all data
|
||||
collection = append(collection, data...)
|
||||
} else {
|
||||
// input should be a valid JSON
|
||||
var (
|
||||
data JSONArray
|
||||
input = []byte(arg)
|
||||
err error
|
||||
)
|
||||
if !json.Valid(input) {
|
||||
log.Error().Msgf("argument %d not a valid JSON", i)
|
||||
continue
|
||||
}
|
||||
err = json.Unmarshal(input, &data)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed to unmarshal input for argument %d", i)
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
return collection
|
||||
}
|
||||
|
||||
func handleArgs(args []string) []map[string]any {
|
||||
// JSON representation
|
||||
type (
|
||||
JSONObject = map[string]any
|
||||
JSONArray = []JSONObject
|
||||
)
|
||||
// no file to load, so we just use the joined args (since each one is a new line)
|
||||
// and then stop
|
||||
var (
|
||||
collection JSONArray
|
||||
data []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if len(sendDataArgs) > 0 {
|
||||
return nil
|
||||
}
|
||||
data, err = ReadStdin()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to read from standard input")
|
||||
return nil
|
||||
}
|
||||
if len(data) == 0 {
|
||||
log.Warn().Msg("no data found from standard input")
|
||||
return nil
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
collection, err = parseInput([]byte(data))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to validate input from arg")
|
||||
}
|
||||
return collection
|
||||
}
|
||||
|
||||
func parseInput(contents []byte) ([]map[string]any, error) {
|
||||
var (
|
||||
data []map[string]any
|
||||
err error
|
||||
)
|
||||
|
||||
// convert/validate JSON input format
|
||||
switch sendInputFormat {
|
||||
case FORMAT_JSON:
|
||||
err = json.Unmarshal(contents, &data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal input in JSON: %v", err)
|
||||
}
|
||||
case FORMAT_YAML:
|
||||
err = yaml.Unmarshal(contents, &data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal input in YAML: %v", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unrecognized format")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ReadStdin reads all of standard input and returns the bytes. If an error
|
||||
// occurs during scanning, it is returned.
|
||||
func ReadStdin() ([]byte, error) {
|
||||
var b []byte
|
||||
input := bufio.NewScanner(os.Stdin)
|
||||
for input.Scan() {
|
||||
b = append(b, input.Bytes()...)
|
||||
b = append(b, byte('\n'))
|
||||
if len(b) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := input.Err(); err != nil {
|
||||
return b, fmt.Errorf("failed to read stdin: %w", err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue