feat: initial implementation of command split

This commit is contained in:
David Allen 2025-04-21 08:47:52 -06:00
parent 522ddb985d
commit 835b678e75
Signed by: towk
GPG key ID: 793B2924A49B3A3F
8 changed files with 181 additions and 63 deletions

View file

@ -74,6 +74,7 @@ var CollectCmd = &cobra.Command{
SecretsFile: secretsFile, SecretsFile: secretsFile,
Username: username, Username: username,
Password: password, Password: password,
Format: format,
} }
// show all of the 'collect' parameters being set from CLI if verbose // show all of the 'collect' parameters being set from CLI if verbose
@ -122,15 +123,17 @@ var CollectCmd = &cobra.Command{
func init() { func init() {
currentUser, _ = user.Current() currentUser, _ = user.Current()
CollectCmd.PersistentFlags().StringVar(&host, "host", "", "Set the URI to the SMD root endpoint") CollectCmd.Flags().StringVar(&host, "host", "", "Set the URI to the SMD root endpoint")
CollectCmd.PersistentFlags().StringVarP(&username, "username", "u", "", "Set the master BMC username") CollectCmd.Flags().StringVarP(&username, "username", "u", "", "Set the master BMC username")
CollectCmd.PersistentFlags().StringVarP(&password, "password", "p", "", "Set the master BMC password") CollectCmd.Flags().StringVarP(&password, "password", "p", "", "Set the master BMC password")
CollectCmd.PersistentFlags().StringVar(&secretsFile, "secrets-file", "", "Set path to the node secrets file") CollectCmd.Flags().StringVar(&secretsFile, "secrets-file", "", "Set path to the node secrets file")
CollectCmd.PersistentFlags().StringVar(&scheme, "scheme", "https", "Set the default scheme used to query when not included in URI") CollectCmd.Flags().StringVar(&scheme, "scheme", "https", "Set the default scheme used to query when not included in URI")
CollectCmd.PersistentFlags().StringVar(&protocol, "protocol", "tcp", "Set the protocol used to query") CollectCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the protocol used to query")
CollectCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", fmt.Sprintf("/tmp/%smagellan/inventory/", currentUser.Username+"/"), "Set the path to store collection data") CollectCmd.Flags().StringVarP(&outputPath, "output", "o", fmt.Sprintf("/tmp/%smagellan/inventory/", currentUser.Username+"/"), "Set the path to store collection data")
CollectCmd.PersistentFlags().BoolVar(&forceUpdate, "force-update", false, "Set flag to force update data sent to SMD") CollectCmd.Flags().BoolVar(&forceUpdate, "force-update", false, "Set flag to force update data sent to SMD")
CollectCmd.PersistentFlags().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(&format, "format", "F", "hive", "Set the output format (json|yaml)")
CollectCmd.Flags().BoolVar(&useHive, "use-hive", true, "Set the output format")
// set flags to only be used together // set flags to only be used together
CollectCmd.MarkFlagsRequiredTogether("username", "password") CollectCmd.MarkFlagsRequiredTogether("username", "password")
@ -143,6 +146,7 @@ func init() {
checkBindFlagError(viper.BindPFlag("collect.force-update", CollectCmd.Flags().Lookup("force-update"))) checkBindFlagError(viper.BindPFlag("collect.force-update", CollectCmd.Flags().Lookup("force-update")))
checkBindFlagError(viper.BindPFlag("collect.cacert", CollectCmd.Flags().Lookup("cacert"))) checkBindFlagError(viper.BindPFlag("collect.cacert", CollectCmd.Flags().Lookup("cacert")))
checkBindFlagError(viper.BindPFlags(CollectCmd.Flags())) checkBindFlagError(viper.BindPFlags(CollectCmd.Flags()))
checkBindFlagError(viper.BindPFlag("collect.use-hive", CollectCmd.Flags().Lookup("use-hive")))
rootCmd.AddCommand(CollectCmd) rootCmd.AddCommand(CollectCmd)
} }

View file

@ -45,6 +45,7 @@ var (
debug bool debug bool
forceUpdate bool forceUpdate bool
insecure bool insecure bool
useHive bool
) )
// The `root` command doesn't do anything on it's own except display // The `root` command doesn't do anything on it's own except display
@ -79,7 +80,7 @@ func init() {
rootCmd.PersistentFlags().IntVarP(&timeout, "timeout", "t", 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().BoolVar(&debug, "debug", false, "Set to enable/disable debug messages")
rootCmd.PersistentFlags().StringVar(&accessToken, "access-token", "", "Set the access token") 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", currentUser.Username), "Set the scanning result cache path")

98
cmd/send.go Normal file
View file

@ -0,0 +1,98 @@
package cmd
import (
"crypto/x509"
"encoding/json"
"net/http"
"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"
)
var sendData []string
var sendCmd = &cobra.Command{
Use: "send [host]",
Example: ` // send data from collect output
magellan send -d @collected-1.json -d @collected-2.json https://smd.openchami.cluster
magellan send -d '{...}' -d @collected-1.json https://api.exampe.com
`,
Short: "Send collected node information to specified host.",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// concatenate all of the data from `-d` flag to send
var (
smdClient = &client.SmdClient{Client: &http.Client{}}
inputData = []byte(strings.Join(sendData, "\n"))
)
// 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")
}
}
// try and load cert if argument is passed
if cacertPath != "" {
cacert, err := os.ReadFile(cacertPath)
if err != nil {
log.Warn().Err(err).Msg("failed to read cert path")
}
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(cacert)
// smdClient.WithCertPool(certPool)
// client.WithCertPool(smdClient, certPool)
}
// create and set headers for request
headers := client.HTTPHeader{}
headers.Authorization(accessToken)
headers.ContentType("application/json")
// unmarshal into map
data := map[string]any{}
err := json.Unmarshal(inputData, &data)
if err != nil {
log.Error().Err(err).Msg("failed to unmarshal data to make request")
}
for _, host := range args {
host, err := urlx.Sanitize(host)
if err != nil {
log.Error().Err(err).Msg("failed to sanitize host")
}
smdClient.URI = host
err = smdClient.Add(inputData, headers)
if err != nil {
// try updating instead
if forceUpdate {
smdClient.Xname = data["ID"].(string)
err = smdClient.Update(inputData, headers)
if err != nil {
log.Error().Err(err).Msgf("failed to forcibly update Redfish endpoint")
}
} else {
log.Error().Err(err).Msgf("failed to add Redfish endpoint")
}
}
}
},
}
func init() {
sendCmd.Flags().StringSliceVarP(&sendData, "data", "d", []string{}, "Set the data to send to specified host.")
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)
}

2
go.mod
View file

@ -19,6 +19,7 @@ require (
require ( require (
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
golang.org/x/crypto v0.32.0 golang.org/x/crypto v0.32.0
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
@ -56,5 +57,4 @@ require (
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.21.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View file

@ -9,8 +9,6 @@ import (
"net/http" "net/http"
"os" "os"
"time" "time"
"github.com/rs/zerolog/log"
) )
type Option[T Client] func(client *T) type Option[T Client] func(client *T)
@ -38,47 +36,47 @@ func NewClient[T Client](opts ...func(T)) T {
return *client return *client
} }
func WithCertPool[T Client](certPool *x509.CertPool) func(T) { func WithCertPool(client Client, certPool *x509.CertPool) error {
// make sure we have a valid cert pool // make sure we have a valid cert pool
if certPool == nil { if certPool == nil {
return func(client T) {} return fmt.Errorf("invalid cert pool")
} }
return func(client T) {
// make sure that we can access the internal client // make sure that we can access the internal client
if client.GetInternalClient() == nil { internalClient := client.GetInternalClient()
log.Warn().Any("client", client.GetInternalClient()).Msg("invalid internal HTTP client ()") if internalClient == nil {
return return fmt.Errorf("invalid HTTP client")
}
client.GetInternalClient().Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
InsecureSkipVerify: true,
},
DisableKeepAlives: true,
Dial: (&net.Dialer{
Timeout: 120 * time.Second,
KeepAlive: 120 * time.Second,
}).Dial,
TLSHandshakeTimeout: 120 * time.Second,
ResponseHeaderTimeout: 120 * time.Second,
}
} }
internalClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
InsecureSkipVerify: true,
},
DisableKeepAlives: true,
Dial: (&net.Dialer{
Timeout: 120 * time.Second,
KeepAlive: 120 * time.Second,
}).Dial,
TLSHandshakeTimeout: 120 * time.Second,
ResponseHeaderTimeout: 120 * time.Second,
}
return nil
} }
func WithSecureTLS[T Client](certPath string) func(T) { func WithSecureTLS(client Client, certPath string) error {
cacert, err := os.ReadFile(certPath) cacert, err := os.ReadFile(certPath)
if err != nil { if err != nil {
return func(client T) {} return fmt.Errorf("failed to read certificate from path '%s': %v", certPath, err)
} }
certPool := x509.NewCertPool() certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(cacert) certPool.AppendCertsFromPEM(cacert)
return WithCertPool[T](certPool) return WithCertPool(client, certPool)
} }
// Post() is a simplified wrapper function that packages all of the // Post() is a simplified wrapper function that packages all of the
// that marshals a mapper into a JSON-formatted byte array, and then performs // that marshals a mapper into a JSON-formatted byte array, and then performs
// a request to the specified URL. // a request to the specified URL.
func (c *MagellanClient) Post(url string, data map[string]any, header HTTPHeader) (*http.Response, HTTPBody, error) { func (c *DefaultClient) Post(url string, data map[string]any, header HTTPHeader) (*http.Response, HTTPBody, error) {
// serialize data into byte array // serialize data into byte array
body, err := json.Marshal(data) body, err := json.Marshal(data)
if err != nil { if err != nil {

View file

@ -5,11 +5,11 @@ import (
"net/http" "net/http"
) )
type MagellanClient struct { type DefaultClient struct {
*http.Client *http.Client
} }
func (c *MagellanClient) Name() string { func (c *DefaultClient) Name() string {
return "default" return "default"
} }
@ -18,7 +18,7 @@ func (c *MagellanClient) Name() string {
// the first argument with no data processing or manipulation. The function sends // the first argument with no data processing or manipulation. The function sends
// the data to a set callback URL (which may be changed to use a configurable value // the data to a set callback URL (which may be changed to use a configurable value
// instead). // instead).
func (c *MagellanClient) Add(data HTTPBody, headers HTTPHeader) error { func (c *DefaultClient) Add(data HTTPBody, headers HTTPHeader) error {
if data == nil { if data == nil {
return fmt.Errorf("no data found") return fmt.Errorf("no data found")
} }
@ -35,7 +35,7 @@ func (c *MagellanClient) Add(data HTTPBody, headers HTTPHeader) error {
return err return err
} }
func (c *MagellanClient) Update(data HTTPBody, headers HTTPHeader) error { func (c *DefaultClient) Update(data HTTPBody, headers HTTPHeader) error {
if data == nil { if data == nil {
return fmt.Errorf("no data found") return fmt.Errorf("no data found")
} }

View file

@ -18,6 +18,7 @@ import (
"github.com/OpenCHAMI/magellan/pkg/client" "github.com/OpenCHAMI/magellan/pkg/client"
"github.com/OpenCHAMI/magellan/pkg/crawler" "github.com/OpenCHAMI/magellan/pkg/crawler"
"github.com/OpenCHAMI/magellan/pkg/secrets" "github.com/OpenCHAMI/magellan/pkg/secrets"
"gopkg.in/yaml.v3"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -39,6 +40,7 @@ type CollectParams struct {
CaCertPath string // set the cert path with the 'cacert' flag CaCertPath string // set the cert path with the 'cacert' flag
Verbose bool // set whether to include verbose output with 'verbose' flag Verbose bool // set whether to include verbose output with 'verbose' flag
OutputPath string // set the path to save output with 'output' flag OutputPath string // set the path to save output with 'output' flag
Format string // set the output format
ForceUpdate bool // set whether to force updating SMD with 'force-update' flag ForceUpdate bool // set whether to force updating SMD with 'force-update' flag
AccessToken string // set the access token to include in request with 'access-token' flag AccessToken string // set the access token to include in request with 'access-token' flag
SecretsFile string // set the path to secrets file SecretsFile string // set the path to secrets file
@ -191,9 +193,18 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams, localStore s
headers.Authorization(params.AccessToken) headers.Authorization(params.AccessToken)
headers.ContentType("application/json") headers.ContentType("application/json")
body, err := json.MarshalIndent(data, "", " ") var body []byte
if err != nil { switch params.Format {
log.Error().Err(err).Msgf("failed to marshal output to JSON") case "json":
body, err = json.MarshalIndent(data, "", " ")
if err != nil {
log.Error().Err(err).Msgf("failed to marshal output to JSON")
}
case "yaml":
body, err = yaml.Marshal(data)
if err != nil {
log.Error().Err(err).Msgf("failed to marshal output to YAML")
}
} }
if params.Verbose { if params.Verbose {
@ -203,22 +214,28 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams, localStore s
// add data output to collections // add data output to collections
collection = append(collection, data) collection = append(collection, data)
// write JSON data to file if output path is set using hive partitioning strategy // write data to file if output path is set using set format
if outputPath != "" { if outputPath != "" {
var ( switch params.Format {
finalPath = fmt.Sprintf("./%s/%s/%d.json", outputPath, data["ID"], time.Now().Unix()) case "hive":
finalDir = filepath.Dir(finalPath) var (
) finalPath = fmt.Sprintf("./%s/%s/%d.json", outputPath, data["ID"], time.Now().Unix())
// if it doesn't, make the directory and write file finalDir = filepath.Dir(finalPath)
err = os.MkdirAll(finalDir, 0o777) )
if err == nil { // no error // if it doesn't, make the directory and write file
err = os.WriteFile(path.Clean(finalPath), body, os.ModePerm) err = os.MkdirAll(finalDir, 0o777)
if err != nil { if err == nil { // no error
log.Error().Err(err).Msgf("failed to write collect output to file") err = os.WriteFile(path.Clean(finalPath), body, os.ModePerm)
if err != nil {
log.Error().Err(err).Msgf("failed to write collect output to file")
}
} else { // error is set
log.Error().Err(err).Msg("failed to make directory for collect output")
} }
case "json":
case "yaml":
} else { // error is set default:
log.Error().Err(err).Msg("failed to make directory for collect output")
} }
} }

View file

@ -126,17 +126,17 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) {
// Obtain the ServiceRoot // Obtain the ServiceRoot
rf_service := client.GetService() rf_service := client.GetService()
log.Info().Msgf("found ServiceRoot %s. Redfish Version %s", rf_service.ID, rf_service.RedfishVersion) log.Debug().Msgf("found ServiceRoot %s. Redfish Version %s", rf_service.ID, rf_service.RedfishVersion)
// Nodes are sometimes only found under Chassis, but they should be found under Systems. // Nodes are sometimes only found under Chassis, but they should be found under Systems.
rf_chassis, err := rf_service.Chassis() rf_chassis, err := rf_service.Chassis()
if err == nil { if err == nil {
log.Info().Msgf("found %d chassis in ServiceRoot", len(rf_chassis)) log.Debug().Msgf("found %d chassis in ServiceRoot", len(rf_chassis))
for _, chassis := range rf_chassis { for _, chassis := range rf_chassis {
rf_chassis_systems, err := chassis.ComputerSystems() rf_chassis_systems, err := chassis.ComputerSystems()
if err == nil { if err == nil {
rf_systems = append(rf_systems, rf_chassis_systems...) rf_systems = append(rf_systems, rf_chassis_systems...)
log.Info().Msgf("found %d systems in chassis %s", len(rf_chassis_systems), chassis.ID) log.Debug().Msgf("found %d systems in chassis %s", len(rf_chassis_systems), chassis.ID)
} }
} }
} }
@ -144,7 +144,7 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) {
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to get systems from ServiceRoot") log.Error().Err(err).Msg("failed to get systems from ServiceRoot")
} }
log.Info().Msgf("found %d systems in ServiceRoot", len(rf_root_systems)) log.Debug().Msgf("found %d systems in ServiceRoot", len(rf_root_systems))
rf_systems = append(rf_systems, rf_root_systems...) rf_systems = append(rf_systems, rf_root_systems...)
return walkSystems(rf_systems, nil, config.URI) return walkSystems(rf_systems, nil, config.URI)
} }
@ -202,7 +202,7 @@ func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) {
// Obtain the ServiceRoot // Obtain the ServiceRoot
rf_service := client.GetService() rf_service := client.GetService()
log.Info().Msgf("found ServiceRoot %s. Redfish Version %s", rf_service.ID, rf_service.RedfishVersion) log.Debug().Msgf("found ServiceRoot %s. Redfish Version %s", rf_service.ID, rf_service.RedfishVersion)
rf_managers, err := rf_service.Managers() rf_managers, err := rf_service.Managers()
if err != nil { if err != nil {