diff --git a/.gitignore b/.gitignore index aeafd79..74ffb61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ magellan emulator/rf-emulator +dist/* **/*.db **.tar.gz **.tar.zst **.part -dist/* +**.txt +**.json **coverage.out** -magellan.1 \ No newline at end of file +magellan.1 diff --git a/cmd/collect.go b/cmd/collect.go index abd3c49..e1ed3d3 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -5,7 +5,7 @@ import ( "github.com/cznic/mathutil" "github.com/davidallendj/magellan/internal/cache/sqlite" - urlx "github.com/davidallendj/magellan/internal/url" + urlx "github.com/davidallendj/magellan/internal/urlx" magellan "github.com/davidallendj/magellan/pkg" "github.com/davidallendj/magellan/pkg/auth" "github.com/davidallendj/magellan/pkg/bmc" diff --git a/cmd/crawl.go b/cmd/crawl.go index 875794d..038e6be 100644 --- a/cmd/crawl.go +++ b/cmd/crawl.go @@ -8,7 +8,7 @@ import ( "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" - urlx "github.com/davidallendj/magellan/internal/url" + urlx "github.com/davidallendj/magellan/internal/urlx" "github.com/davidallendj/magellan/pkg/bmc" "github.com/davidallendj/magellan/pkg/crawler" "github.com/davidallendj/magellan/pkg/secrets" diff --git a/cmd/list.go b/cmd/list.go index e82b4a1..dd11449 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,13 +1,16 @@ package cmd import ( - "encoding/json" "fmt" "os" "strings" "time" "github.com/davidallendj/magellan/internal/cache/sqlite" + urlx "github.com/davidallendj/magellan/internal/urlx" + magellan "github.com/davidallendj/magellan/pkg" + "github.com/davidallendj/magellan/pkg/crawler" + "github.com/davidallendj/magellan/pkg/secrets" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" @@ -34,15 +37,17 @@ var ListCmd = &cobra.Command{ "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 { - fmt.Printf("cache: %s\n", cachePath) - return + if showCacheInfo { + magellan.PrintMapFormat(map[string]any{ + "path": cachePath, + }, format) + os.Exit(0) } // load the assets found from scan scannedResults, err := sqlite.GetScannedAssets(cachePath) if err != nil { - log.Error().Err(err).Msg("failed to get scanned assets") + log.Error().Err(err).Str("path", cachePath).Msg("failed to get scanned assets from cache") } switch strings.ToLower(listOutputFormat) { case FORMAT_JSON: @@ -65,6 +70,25 @@ var ListCmd = &cobra.Command{ log.Error().Msg("unrecognized format") os.Exit(1) } + args[0], err = urlx.Sanitize(args[0]) + if err != nil { + return fmt.Errorf("failed to sanitize URI: %w", err) + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + store := secrets.NewStaticStore(listUsername, listPassword) + drives, err := magellan.ListDrives(&crawler.CrawlerConfig{ + URI: args[0], + CredentialStore: store, //initSecretsStore(args[0]), + Insecure: insecure, + UseDefault: false, + }) + if err != nil { + log.Error().Err(err).Msg("failed to get drives") + os.Exit(1) + } + magellan.PrintDrives(drives) }, } diff --git a/cmd/root.go b/cmd/root.go index 360d680..e74f5eb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,6 +21,8 @@ import ( magellan "github.com/davidallendj/magellan/internal" "github.com/davidallendj/magellan/internal/util" + "github.com/davidallendj/magellan/pkg/bmc" + "github.com/davidallendj/magellan/pkg/secrets" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -150,3 +152,44 @@ func SetDefaults() { viper.SetDefault("update.status", false) } + +func initSecretsStore(uri string) secrets.SecretStore { + var ( + store secrets.SecretStore + err error + ) + if username != "" && password != "" { + // First, try and load credentials from --username and --password if both are set. + log.Debug().Str("id", uri).Msgf("--username and --password specified, using them for BMC credentials") + store = secrets.NewStaticStore(username, password) + } else { + // Alternatively, locate specific credentials (falling back to default) and override those + // with --username or --password if either are passed. + log.Debug().Str("id", uri).Msgf("one or both of --username and --password NOT passed, attempting to obtain missing credentials from secret store at %s", secretsFile) + if store, err = secrets.OpenStore(secretsFile); err != nil { + log.Error().Str("id", uri).Err(err).Msg("failed to open local secrets store") + } + + // Either none of the flags were passed or only one of them were; get + // credentials from secrets store to fill in the gaps. + bmcCreds, _ := bmc.GetBMCCredentials(store, uri) + nodeCreds := secrets.StaticStore{ + Username: bmcCreds.Username, + Password: bmcCreds.Password, + } + + // If either of the flags were passed, override the fetched + // credentials with them. + if username != "" { + log.Info().Str("id", uri).Msg("--username was set, overriding username for this BMC") + nodeCreds.Username = username + } + if password != "" { + log.Info().Str("id", uri).Msg("--password was set, overriding password for this BMC") + nodeCreds.Password = password + } + + store = &nodeCreds + } + return store +} diff --git a/cmd/scan.go b/cmd/scan.go index 9d4bca4..a005dab 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -12,7 +12,7 @@ import ( "github.com/rs/zerolog/log" "github.com/cznic/mathutil" - urlx "github.com/davidallendj/magellan/internal/url" + urlx "github.com/davidallendj/magellan/internal/urlx" "github.com/spf13/cobra" "github.com/spf13/viper" ) diff --git a/cmd/update.go b/cmd/update.go index c2cc3a4..c537471 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -128,9 +128,7 @@ func init() { 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"))) rootCmd.AddCommand(UpdateCmd) diff --git a/go.mod b/go.mod index 5c84807..4c50d3a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/davidallendj/magellan -go 1.21 +go 1.23.0 + +toolchain go1.24.2 require ( github.com/Cray-HPE/hms-xname v1.3.0 @@ -17,6 +19,9 @@ require ( ) require ( + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.4 + github.com/charmbracelet/lipgloss v1.1.0 github.com/rs/zerolog v1.33.0 golang.org/x/crypto v0.32.0 gopkg.in/yaml.v3 v3.0.1 @@ -28,7 +33,13 @@ require ( ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -39,11 +50,18 @@ require ( github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -51,9 +69,11 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 5ca9e34..9521485 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,26 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Cray-HPE/hms-xname v1.3.0 h1:DQmetMniubqcaL6Cxarz9+7KFfWGSEizIhfPHIgC3Gw= github.com/Cray-HPE/hms-xname v1.3.0/go.mod h1:XKdjQSzoTps5KDOE8yWojBTAWASGaS6LfRrVDxwTQO8= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= +github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= @@ -13,6 +33,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -54,6 +76,8 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -62,10 +86,20 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -77,6 +111,9 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -114,6 +151,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -177,9 +216,12 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -192,6 +234,7 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= ======= @@ -233,6 +276,10 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= >>>>>>> d34ba3f (chore: update references and imports) +======= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +>>>>>>> 05e5c4b (Add support for storage command and crawler output) golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -281,6 +328,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cache/edit/modify.go b/internal/cache/edit/modify.go new file mode 100644 index 0000000..08bf029 --- /dev/null +++ b/internal/cache/edit/modify.go @@ -0,0 +1 @@ +package cache diff --git a/internal/cache/edit/table.go b/internal/cache/edit/table.go new file mode 100644 index 0000000..7759bd7 --- /dev/null +++ b/internal/cache/edit/table.go @@ -0,0 +1,48 @@ +package cache + +import ( + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + +type Model struct { + selected int + Table table.Model +} + +func (m Model) Init() tea.Cmd { return nil } + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.WindowSizeMsg: + // m.Table = m.Table.Width(msg.Width) + // m.Table = m.Table.Height(msg.Height) + case tea.KeyMsg: + switch msg.String() { + case "esc": + if m.Table.Focused() { + m.Table.Blur() + } else { + m.Table.Focus() + } + case "q", "ctrl+c": + return m, tea.Quit + case "enter": + return m, tea.Batch( + tea.Printf("Selected host '%s'", m.Table.SelectedRow()[0]), + ) + } + } + m.Table, cmd = m.Table.Update(msg) + return m, cmd +} + +func (m Model) View() string { + return baseStyle.Render(m.Table.View()) + "\n" +} diff --git a/internal/url/url.go b/internal/urlx/url.go similarity index 99% rename from internal/url/url.go rename to internal/urlx/url.go index be65d5b..c4dbe38 100644 --- a/internal/url/url.go +++ b/internal/urlx/url.go @@ -1,4 +1,4 @@ -package url +package urlx import ( "fmt" diff --git a/internal/util/error.go b/internal/util/error.go index 0141ea7..85de514 100644 --- a/internal/util/error.go +++ b/internal/util/error.go @@ -1,6 +1,8 @@ package util -import "fmt" +import ( + "fmt" +) // FormatErrorList() is a wrapper function that unifies error list formatting // and makes printing error lists consistent. diff --git a/internal/util/print.go b/internal/util/print.go new file mode 100644 index 0000000..aca00de --- /dev/null +++ b/internal/util/print.go @@ -0,0 +1,25 @@ +package util + +import ( + "encoding/json" + "fmt" + + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v2" +) + +func PrintJSON(data any) { + b, err := json.MarshalIndent(data, "", " ") + if err != nil { + log.Error().Err(err).Msgf("failed to marshal scanned results to JSON") + } + fmt.Println(string(b)) +} + +func PrintYAML(data any) { + b, err := yaml.Marshal(data) + if err != nil { + log.Error().Err(err).Msgf("failed to marshal scanned results to JSON") + } + fmt.Printf("%s\n", string(b)) +} diff --git a/internal/util/strings.go b/internal/util/strings.go new file mode 100644 index 0000000..93a046f --- /dev/null +++ b/internal/util/strings.go @@ -0,0 +1,10 @@ +package util + +import "strings" + +func TidyJSON(s string) string { + s = strings.ReplaceAll(s, "\n", "") + s = strings.ReplaceAll(s, "\t", "") + s = strings.ReplaceAll(s, " ", "") + return strings.ReplaceAll(s, "\"", "'") +} diff --git a/pkg/bmc/bmc.go b/pkg/bmc/bmc.go index 90dcadf..c451543 100644 --- a/pkg/bmc/bmc.go +++ b/pkg/bmc/bmc.go @@ -14,6 +14,9 @@ type BMCCredentials struct { func GetBMCCredentialsDefault(store secrets.SecretStore) (BMCCredentials, error) { var creds BMCCredentials + if store == nil { + return creds, fmt.Errorf("invalid secrets store") + } if strCreds, err := store.GetSecretByID(secrets.DEFAULT_KEY); err != nil { return creds, fmt.Errorf("get default BMC credentials from secret store: %w", err) } else { @@ -27,6 +30,9 @@ func GetBMCCredentialsDefault(store secrets.SecretStore) (BMCCredentials, error) func GetBMCCredentials(store secrets.SecretStore, id string) (BMCCredentials, error) { var creds BMCCredentials + if store == nil { + return creds, fmt.Errorf("invalid secrets store") + } if strCreds, err := store.GetSecretByID(id); err != nil { return creds, fmt.Errorf("get BMC credentials from secret store: %w", err) } else { diff --git a/pkg/collect.go b/pkg/collect.go index 56c3348..0be1cf7 100644 --- a/pkg/collect.go +++ b/pkg/collect.go @@ -97,6 +97,7 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams, store secret var ( systems []crawler.InventoryDetail managers []crawler.Manager + storage []crawler.Storage config = crawler.CrawlerConfig{ URI: uri, CredentialStore: params.SecretStore, @@ -116,6 +117,11 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams, store secret log.Error().Err(err).Str("uri", uri).Msg("failed to crawl BMC for managers") } + storage, err = crawler.CrawlBMCForStorage(config) + if err != nil { + log.Error().Err(err).Str("uri", uri).Msg("failed to crawl BMC for storage") + } + // we didn't find anything so do not proceed if util.IsEmpty(systems) && util.IsEmpty(managers) { continue @@ -144,6 +150,7 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams, store secret "RediscoverOnUpdate": false, "Systems": systems, "Managers": managers, + "Storage": storage, "SchemaVersion": 1, } diff --git a/pkg/crawler/main.go b/pkg/crawler/main.go index 02a6745..4de420f 100644 --- a/pkg/crawler/main.go +++ b/pkg/crawler/main.go @@ -83,6 +83,26 @@ type InventoryDetail struct { Chassis_Model string `json:"chassis_model,omitempty"` // Model of the Chassis } +type Storage struct { + URI string `json:"uri"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + DrivesCount int `json:"drives_count"` + Controllers []StorageController `json:"controllers"` +} + +type StorageController struct { + Identifiers []string `json:"identifiers"` + FirmwareVersion string `json:"firmware_version"` + Location string `json:"location"` + Manufacturer string `json:"manufacturer"` + Model string `json:"model"` + PartNumber string `json:"part_number"` + SerialNumber string `json:"serial_number"` + SpeedGbps float32 `json:"speed_gbps"` +} + // CrawlBMCForSystems pulls all pertinent information from a BMC. It accepts a CrawlerConfig and returns a list of InventoryDetail structs. func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) { var ( @@ -207,6 +227,49 @@ func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) { return walkManagers(rf_managers, config.URI) } +func CrawlBMCForStorage(config CrawlerConfig) ([]Storage, error) { + // get username and password from secret store + bmc_creds, err := loadBMCCreds(config) + if err != nil { + event := log.Error() + event.Err(err) + event.Msg("failed to load BMC credentials") + return nil, err + } + // initialize gofish client + var storage []Storage + client, err := gofish.Connect(gofish.ClientConfig{ + Endpoint: config.URI, + Username: bmc_creds.Username, + Password: bmc_creds.Password, + Insecure: config.Insecure, + BasicAuth: true, + }) + if err != nil { + if strings.HasPrefix(err.Error(), "404:") { + err = fmt.Errorf("no ServiceRoot found. This is probably not a BMC: %s", config.URI) + } + if strings.HasPrefix(err.Error(), "401:") { + err = fmt.Errorf("authentication failed. Check your username and password: %s", config.URI) + } + event := log.Error() + event.Err(err) + event.Msg("failed to connect to BMC") + return storage, err + } + defer client.Logout() + + // Obtain the ServiceRoot + rf_service := client.GetService() + log.Debug().Msgf("found ServiceRoot %s. Redfish Version %s", rf_service.ID, rf_service.RedfishVersion) + + rf_storage, err := rf_service.Storage() + if err != nil { + log.Error().Err(err).Msg("failed to get managers from ServiceRoot") + } + return walkStorage(rf_storage, config.URI) +} + // walkSystems processes a list of Redfish computer systems and their associated chassis, // and returns a list of inventory details for each system. // @@ -369,6 +432,40 @@ func walkManagers(rf_managers []*redfish.Manager, baseURI string) ([]Manager, er return managers, nil } +func walkStorage(rf_storage []*redfish.Storage, baseURI string) ([]Storage, error) { + var storage []Storage + for _, rf_storage := range rf_storage { + var controllers []StorageController + var rf_storage_controllers, err = rf_storage.Controllers() + if err != nil { + log.Error().Err(err).Msg("failed to get storage controllers") + } else { + for _, controller := range rf_storage_controllers { + controllers = append(controllers, StorageController{ + // Identifiers: controller.Identifiers, + FirmwareVersion: controller.FirmwareVersion, + Location: controller.Location.Info, + Manufacturer: controller.Manufacturer, + Model: controller.Model, + PartNumber: controller.PartNumber, + SerialNumber: controller.SerialNumber, + SpeedGbps: controller.SpeedGbps, + }) + } + } + + storage = append(storage, Storage{ + URI: baseURI + "/redfish/v1/Storage/" + rf_storage.ID, + ID: rf_storage.ID, + Name: rf_storage.Name, + Description: rf_storage.Description, + DrivesCount: rf_storage.DrivesCount, + Controllers: controllers, + }) + } + return storage, nil +} + func loadBMCCreds(config CrawlerConfig) (bmc.BMCCredentials, error) { // NOTE: it is possible for the SecretStore to be nil, so we need a check if config.CredentialStore == nil { diff --git a/pkg/list.go b/pkg/list.go new file mode 100644 index 0000000..5b2e5fa --- /dev/null +++ b/pkg/list.go @@ -0,0 +1,100 @@ +package magellan + +import ( + "fmt" + "strings" + "time" + + "github.com/davidallendj/magellan/internal/util" + "github.com/davidallendj/magellan/pkg/crawler" + "github.com/rs/zerolog/log" + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/redfish" +) + +func PrintRemoteAssets(data []RemoteAsset, format string) { + switch strings.ToLower(format) { + case "json": + util.PrintJSON(data) + case "yaml": + util.PrintYAML(data) + case "none": + for _, r := range data { + fmt.Printf("%s:%d (%s) @%s\n", r.Host, r.Port, r.Protocol, r.Timestamp.Format(time.UnixDate)) + } + default: + log.Error().Msg("unrecognized format") + } +} + +func PrintMapFormat(data map[string]any, format string) { + switch strings.ToLower(format) { + case "json": + util.PrintJSON(data) + case "yaml": + util.PrintYAML(data) + case "none": + for k, v := range data { + fmt.Printf("%s: %v\n", k, v) + } + default: + log.Error().Msg("unrecognized format") + } +} + +func ListDrives(cc *crawler.CrawlerConfig) ([]*redfish.Drive, error) { + user, err := cc.GetUserPass() + if err != nil { + return nil, fmt.Errorf("failed to get username and password: %v", err) + } + log.Info().Str("username", user.Username).Str("password", user.Password).Str("host", cc.URI).Msg("credentials used") + // Create a new instance of gofish client, ignoring self-signed certs + c, err := gofish.Connect(gofish.ClientConfig{ + Endpoint: cc.URI, + Username: user.Username, + Password: user.Password, + Insecure: cc.Insecure, + BasicAuth: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to connect to host: %v", util.TidyJSON(err.Error())) + } + defer c.Logout() + + // Retrieve the service root + systems, err := c.Service.Systems() + if err != nil { + return nil, fmt.Errorf("failed to retrieve systems: %v", err) + } + + // aggregate all of the drives together + foundDrives := []*redfish.Drive{} + for _, system := range systems { + storage, err := system.Storage() + if err != nil { + continue + } + + for _, ss := range storage { + drives, err := ss.Drives() + if err != nil { + continue + } + + foundDrives = append(foundDrives, drives...) + } + } + return foundDrives, nil +} + +func PrintDrives(drives []*redfish.Drive) { + for i, drive := range drives { + fmt.Printf("Drive %d\n", i) + fmt.Printf("\tManufacturer: %s\n", drive.Manufacturer) + fmt.Printf("\tModel: %s\n", drive.Model) + fmt.Printf("\tSize: %d GiB\n", (drive.CapacityBytes / 1024 / 1024 / 1024)) + fmt.Printf("\tSerial number: %s\n", drive.SerialNumber) + fmt.Printf("\tPart number: %s\n", drive.PartNumber) + fmt.Printf("\tLocation: %s %d\n", drive.PhysicalLocation.PartLocation.LocationType, drive.PhysicalLocation.PartLocation.LocationOrdinalValue) + } +} diff --git a/pkg/scan.go b/pkg/scan.go index c1b5f13..032e75f 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -10,7 +10,7 @@ import ( "sync" "time" - urlx "github.com/davidallendj/magellan/internal/url" + urlx "github.com/davidallendj/magellan/internal/urlx" "github.com/davidallendj/magellan/pkg/client" "github.com/rs/zerolog/log" )