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
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