From 6f2ca76d4526a3a5a454f895e0ffbd306bb5efb9 Mon Sep 17 00:00:00 2001 From: David Allen Date: Tue, 7 Oct 2025 19:26:15 -0600 Subject: [PATCH 01/15] chore: minor changes and added debug prints Signed-off-by: David J. Allen Signed-off-by: David Allen --- cmd/crawl.go | 5 ++--- cmd/scan.go | 1 - internal/util/path.go | 17 +++++++++++++++++ pkg/crawler/main.go | 3 ++- pkg/scan.go | 26 +++++++++++++++++++++----- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/cmd/crawl.go b/cmd/crawl.go index 71382061..01358e01 100644 --- a/cmd/crawl.go +++ b/cmd/crawl.go @@ -3,7 +3,6 @@ package cmd import ( "encoding/json" "fmt" - "os" "github.com/rs/zerolog/log" @@ -57,7 +56,7 @@ var CrawlCmd = &cobra.Command{ log.Debug().Str("uri", 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("uri", uri).Err(err).Msg("failed to open local secrets store") - os.Exit(1) + return } // Either none of the flags were passed or only one of them were; get @@ -137,7 +136,7 @@ var CrawlCmd = &cobra.Command{ }, crawlOutputFormat) if err != nil { log.Error().Err(err).Msg("failed to marshal output JSON") - os.Exit(1) + return } if showOutput { fmt.Println(string(output)) diff --git a/cmd/scan.go b/cmd/scan.go index c9f401e3..770b73dd 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -140,7 +140,6 @@ var ScanCmd = &cobra.Command{ log.Trace().Any("assets", foundAssets).Msgf("found assets from scan") } else { log.Warn().Msg("no responsive assets found") - // return instead of exit to close log file return } diff --git a/internal/util/path.go b/internal/util/path.go index 2541cc60..de639540 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -4,6 +4,8 @@ import ( "fmt" "io/fs" "os" + "path/filepath" + "strings" "time" ) @@ -17,6 +19,21 @@ func PathExists(path string) (fs.FileInfo, bool) { return fi, !os.IsNotExist(err) } +// SplitPathForViper() is an utility function to split a path into 3 parts: +// - directory +// - filename +// - extension +// The intent was to break a path into a format that's more easily consumable +// by spf13/viper's API. See the "LoadConfig()" function in internal/config.go +// for more details. +// +// TODO: Rename function to something more generalized. +func SplitPathForViper(path string) (string, string, string) { + filename := filepath.Base(path) + ext := filepath.Ext(filename) + return filepath.Dir(path), strings.TrimSuffix(filename, ext), strings.TrimPrefix(ext, ".") +} + // MakeOutputDirectory() creates a new directory at the path argument if // the path does not exist. // diff --git a/pkg/crawler/main.go b/pkg/crawler/main.go index 681af718..c865706f 100644 --- a/pkg/crawler/main.go +++ b/pkg/crawler/main.go @@ -147,7 +147,8 @@ func GetBMCClient(config CrawlerConfig) (*gofish.APIClient, error) { return client, nil } -// CrawlBMCForSystems pulls all pertinent information from a BMC. It accepts a CrawlerConfig and returns a list of InventoryDetail structs. +// 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 ( systems = make(map[string]*InventoryDetail) diff --git a/pkg/scan.go b/pkg/scan.go index 603cb0a5..5cd6f99c 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -65,7 +65,7 @@ type ScanParams struct { // Returns a list of scanned results to be stored in cache (but isn't doing here). func ScanForAssets(params *ScanParams) []RemoteAsset { var ( - results = make([]RemoteAsset, 0, len(params.TargetHosts)) + results []RemoteAsset done = make(chan struct{}, params.Concurrency+1) chanHosts = make(chan []string, params.Concurrency+1) ) @@ -116,29 +116,45 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { for _, foundAsset := range foundAssets { for _, probe := range probesToRun { probeURL := fmt.Sprintf("%s:%d%s", foundAsset.Host, foundAsset.Port, probe.Path) - req, err := http.NewRequest("GET", probeURL, nil) + req, err := http.NewRequest(http.MethodGet, probeURL, nil) if err != nil { + log.Warn(). + Err(err). + Str("uri", probeURL). + Msg("could not make probing request") continue } res, err := probeClient.Do(req) if err == nil && res != nil && res.StatusCode == http.StatusOK { if err := res.Body.Close(); err != nil { - log.Warn().Err(err).Msg("could not close response resource") + log.Warn(). + Err(err). + Str("url", probeURL). + Msg("could not close response resource") } foundAsset.ServiceType = probe.Type assetsToAdd = append(assetsToAdd, foundAsset) + log.Debug(). + Str("host", foundAsset.Host). + Msg("adding found asset to results after probing") + break // Found a valid service, no need to probe other types } if res != nil { if err := res.Body.Close(); err != nil { - log.Warn().Err(err).Msg("could not close response resource") + log.Warn(). + Err(err). + Msg("could not close response resource") } } } } results = append(results, assetsToAdd...) } else { + log.Debug(). + Int("count", len(foundAssets)). + Msg("adding found assets to results without probing") results = append(results, foundAssets...) } } @@ -162,7 +178,7 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { wg.Wait() close(done) - log.Trace().Msg("scan complete") + log.Debug().Int("asset_count", len(results)).Msg("scan complete") return results } From 7bea6f9a4ef6f50fe16f74c8bda539ae3d6be27e Mon Sep 17 00:00:00 2001 From: David Allen Date: Wed, 8 Oct 2025 08:50:41 -0600 Subject: [PATCH 02/15] fix: issue with chassis/system merge not working Signed-off-by: David J. Allen Signed-off-by: David Allen --- pkg/crawler/main.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pkg/crawler/main.go b/pkg/crawler/main.go index c865706f..0bca4708 100644 --- a/pkg/crawler/main.go +++ b/pkg/crawler/main.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "dario.cat/mergo" "github.com/OpenCHAMI/magellan/internal/util" "github.com/OpenCHAMI/magellan/pkg/bmc" "github.com/OpenCHAMI/magellan/pkg/secrets" @@ -201,15 +200,16 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) { rf_systems = append(rf_systems, rf_root_systems...) newSystems, err := walkSystems(rf_systems, nil, config.URI) if err != nil { - return extract_ptr_map_values(systems), fmt.Errorf("failed to get systems: %v", err) + return extractPtrMapValues(systems), fmt.Errorf("failed to get systems: %v", err) } // If nodes are found under both Chassis and Systems, Systems is assumed to be "more definitive" // and will override corresponding fields from the Chassis version. - err = mergo.Merge(&systems, newSystems, mergo.WithOverride) + // err = mergo.Merge(&systems, newSystems, mergo.WithOverride) + systems, err = merge(systems, newSystems) if err != nil { - return extract_ptr_map_values(systems), fmt.Errorf("failed to merge systems from Chassis and Systems endpoints: %v", err) + return extractPtrMapValues(systems), fmt.Errorf("failed to merge systems from Chassis and Systems endpoints: %v", err) } - return extract_ptr_map_values(systems), nil + return extractPtrMapValues(systems), nil } // CrawlBMCForManagers connects to a BMC (Baseboard Management Controller) using the provided configuration, @@ -515,10 +515,18 @@ func loadBMCCreds(config CrawlerConfig) (bmc.BMCCredentials, error) { } } -func extract_ptr_map_values[T any](m map[string]*T) []T { +func extractPtrMapValues[T any](m map[string]*T) []T { slice := make([]T, 0, len(m)) for i := range m { slice = append(slice, *m[i]) } return slice } + +func merge(systems map[string]*InventoryDetail, newSystems []InventoryDetail) (map[string]*InventoryDetail, error) { + // add and replace values in systems with values from newSystems + for _, system := range newSystems { + systems[system.URI] = &system + } + return systems, nil +} From 2502d7d7ba38eba9c4e8f44c009a07a7c85142a7 Mon Sep 17 00:00:00 2001 From: David Allen Date: Wed, 8 Oct 2025 12:32:58 -0600 Subject: [PATCH 03/15] docs: added info about using --insecure with scan Signed-off-by: David J. Allen Signed-off-by: David Allen --- README.md | 57 +++++++++++++++++++++++++++--------------- cmd/scan.go | 6 ++--- man/magellan-scan.1.sc | 7 +++--- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 10fabcce..e6755fd2 100644 --- a/README.md +++ b/README.md @@ -8,25 +8,26 @@ The `magellan` CLI tool is a Redfish-based, board management controller (BMC) di - [OpenCHAMI Magellan](#openchami-magellan) - - [Main Features](#main-features) - - [Getting Started](#getting-started) - - [Building the Executable](#building-the-executable) - - [Building on Debian 12 (Bookworm)](#building-on-debian-12-bookworm) - - [Docker](#docker) - - [Arch Linux (AUR)](#arch-linux-aur) - - [Usage](#usage) - - [Checking for Redfish](#checking-for-redfish) - - [BMC ID Mapping](#bmc-id-mapping) - - [Running the Tool](#running-the-tool) - - [Managing Secrets](#managing-secrets) - - [Starting the Emulator](#starting-the-emulator) - - [Updating Firmware](#updating-firmware) - - [Managing Power](#managing-power) - - [Getting an Access Token (WIP)](#getting-an-access-token-wip) - - [Running with Docker](#running-with-docker) - - [How It Works](#how-it-works) - - [TODO](#todo) - - [Copyright](#copyright) + - [Main Features](#main-features) + - [Getting Started](#getting-started) + - [Documentation](#documentation) + - [Building the Executable](#building-the-executable) + - [Building on Debian 12 (Bookworm)](#building-on-debian-12-bookworm) + - [Docker](#docker) + - [Arch Linux (AUR)](#arch-linux-aur) + - [Usage](#usage) + - [Checking for Redfish](#checking-for-redfish) + - [BMC ID Mapping](#bmc-id-mapping) + - [Running the Tool](#running-the-tool) + - [PDU Inventory Collection](#pdu-inventory-collection) + - [Starting the Emulator](#starting-the-emulator) + - [Updating Firmware](#updating-firmware) + - [Managing Power](#managing-power) + - [Getting an Access Token (WIP)](#getting-an-access-token-wip) + - [Running with Docker](#running-with-docker) + - [How It Works](#how-it-works) + - [TODO](#todo) + - [Copyright](#copyright) @@ -50,6 +51,20 @@ See the [TODO](#todo) section for a list of soon-ish goals planned. [Build](#building) and [run on bare metal](#running-the-tool) or run and test with Docker using the [latest prebuilt image](#running-with-docker). For quick testing, the repository integrates a Redfish emulator that can be run by executing the `emulator/setup.sh` script or running `make emulator`. +## Documentation + +There is detailed documentation included with the project's repository that can be built using `scdoc` and `go doc`. + +To build the documentation, invoke the following commands. + +```bash +# man page documentation +make man + +# API reference documentation +make docs +``` + ## Building the Executable The `magellan` tool can be built to run on bare metal. Install the required Go tools, clone the repo, and then build the binary in the root directory with the following: @@ -158,7 +173,6 @@ Where the `map_key` is the name of the attribute known to `magellan` that identi xcsb ``` - where `` is a cabinet number in the cluster, `` is a chassis within the cabinet, `` is the shelf within the chassis and `` is the blade within a shelf where the BMC is located. The above mapping file (minus the elipsis) will work with the example described in the [Starting the Emulator](#starting-the-emulator) section. If you are using `magellan` within a system deployed using RIE in the [Quickstart Deployment Recipe](https://github.com/OpenCHAMI/deployment-recipes/blob/main/quickstart/README.md) you can generate a BMC ID Map with XNAMEs that match the RIE configured XNAMEs from the RIE instances running under `docker-compose`. You can do this outside of the docker containers by running this script: @@ -240,6 +254,9 @@ To start a network scan for BMC nodes, use the `scan` command. If the port is no --cache data/assets.db ``` +> [!NOTE] +> Make sure to include the `--insecure` flag if the BMC does not require TLS verification when using HTTPS. + This will scan the `172.16.0.0` subnet returning the host and port that return a response and store the results in a local cache with at the `data/assets.db` path. Additional flags can be set such as `--host` to add more hosts to scan that are not included on the subnet, `--timeout` to set how long to wait for a response from the BMC node, or `--concurrency` to set the number of requests to make concurrently with goroutines. Try using `./magellan help scan` for a complete set of options this subcommand. Alternatively, the same scan can be started using CIDR notation and with additional hosts: ```bash diff --git a/cmd/scan.go b/cmd/scan.go index 770b73dd..01efdf95 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -38,7 +38,7 @@ var ScanCmd = &cobra.Command{ Use: "scan urls...", Example: ` // assumes host https://10.0.0.101:443 - magellan scan 10.0.0.101 + magellan scan 10.0.0.101 --insecure // 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 @@ -47,10 +47,10 @@ var ScanCmd = &cobra.Command{ 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 + magellan scan --subnet 10.0.0.0 -i // assumes subnet using HTTPS and port 443 with specified CIDR - magellan scan --subnet 10.0.0.0/16 + magellan scan --subnet 10.0.0.0/16 -i // 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 diff --git a/man/magellan-scan.1.sc b/man/magellan-scan.1.sc index e9c0c033..9b8e8118 100644 --- a/man/magellan-scan.1.sc +++ b/man/magellan-scan.1.sc @@ -11,7 +11,7 @@ magellan scan [OPTIONS] _host_... # EXAMPLES // assumes host https://10.0.0.101:443++ -magellan scan 10.0.0.101 +magellan scan 10.0.0.101 --insecure // 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 @@ -20,10 +20,10 @@ magellan scan http://10.0.0.101:80 https://$user:$password@10.0.0.102:443 http:/ 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 +magellan scan --subnet 10.0.0.0 -i // assumes subnet using HTTPS and port 443 with specified CIDR++ -magellan scan --subnet 10.0.0.0/16 +magellan scan --subnet 10.0.0.0/16 -i // 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 @@ -83,7 +83,6 @@ magellan scan --subnet 10.0.0.0 --subnet 172.16.0.0 --subnet-mask 255.255.0.0 -- It is recommended that the *--insecure* flag be set to *true* in when the BMC does not require TLS verification for HTTPS requests. - *-o, --output* _path_ Output file path (for json/yaml formats) From bd83ac9596893e054b80149f323523f7eea0800c Mon Sep 17 00:00:00 2001 From: David Allen Date: Wed, 8 Oct 2025 12:35:08 -0600 Subject: [PATCH 04/15] chore: improved log messages Signed-off-by: David J. Allen Signed-off-by: David Allen --- cmd/root.go | 9 ++---- cmd/secrets.go | 75 ++++++++++++++++++++++++++++++++++++-------------- pkg/scan.go | 9 ++++-- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 1d7ef8ca..67aee6b4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -65,7 +65,7 @@ var rootCmd = &cobra.Command{ } }, PostRun: func(cmd *cobra.Command, args []string) { - log.Debug().Msg("closing log file") + log.Debug().Str("path", logFile).Msg("closing log file") err := logger.LogFile.Close() if err != nil { log.Error().Err(err).Msg("failed to close log file") @@ -147,12 +147,7 @@ func InitializeConfig() { viper.SetConfigFile(configPath) } if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - err = fmt.Errorf("config file not found: %w", err) - } else { - err = fmt.Errorf("failed to load config file: %w", err) - } - log.Warn().Err(err).Msg("failed to load config") + log.Debug().Err(err).Msg("failed to load config") } } diff --git a/cmd/secrets.go b/cmd/secrets.go index 7e742c83..070f0c61 100644 --- a/cmd/secrets.go +++ b/cmd/secrets.go @@ -28,7 +28,7 @@ var secretsCmd = &cobra.Command{ magellan secrets store $bmc_host $bmc_creds // retrieve creds from secrets store - magellan secrets retrieve $bmc_host -f nodes.json + magellan secrets retrieve $bmc_host -f secrets.json // list creds from specific secrets magellan secrets list -f nodes.json`, @@ -43,8 +43,8 @@ var secretsGenerateKeyCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { key, err := secrets.GenerateMasterKey() if err != nil { - fmt.Printf("Error generating master key: %v\n", err) - os.Exit(1) + log.Error().Err(err).Msg("failed to generate master key") + return } fmt.Printf("%s\n", key) }, @@ -65,8 +65,8 @@ var secretsStoreCmd = &cobra.Command{ // require either the args or input file if len(args) < 1 && secretsStoreInputFile == "" { - log.Error().Msg("no input data or file") - os.Exit(1) + log.Error().Msg("requires input data or file") + return } else if len(args) > 1 && secretsStoreInputFile == "" { // use args[1] here because args[0] is the secretID secretValue = args[1] @@ -80,6 +80,7 @@ var secretsStoreCmd = &cobra.Command{ username string password string ) + // seperate username and password provided values = strings.Split(secretValue, ":") if len(values) != 2 { @@ -104,19 +105,28 @@ var secretsStoreCmd = &cobra.Command{ case "base64": // format: ($encoded_base64_string) decoded, err := base64.StdEncoding.DecodeString(secretValue) if err != nil { - log.Error().Err(err).Msg("error decoding base64 data") + log.Error(). + Err(err). + Str("path", secretsFile). + Msg("failed to decode base64 data") return } // check the decoded string if it's a valid JSON and has creds if !isValidCredsJSON(string(decoded)) { - log.Error().Err(err).Msg("value is not a valid JSON or is missing credentials") + log.Error(). + Err(err). + Str("path", secretsFile). + Msg("invalid JSON value or is missing credentials") return } store, err = secrets.OpenStore(secretsFile) if err != nil { - log.Error().Err(err).Msg("failed to open secrets store") + log.Error(). + Err(err). + Str("path", secretsFile). + Msg("failed to open secrets store") os.Exit(1) } secretValue = string(decoded) @@ -124,12 +134,16 @@ var secretsStoreCmd = &cobra.Command{ // read input from file if set and override if secretsStoreInputFile != "" { if secretValue != "" { - log.Error().Msg("cannot use -i/--input-file with positional argument") + log.Error(). + Str("input-file", secretsStoreInputFile). + Msg("cannot use -i/--input-file with positional argument") return } inputFileBytes, err = os.ReadFile(secretsStoreInputFile) if err != nil { - log.Error().Err(err).Msg("failed to read input file") + log.Error(). + Err(err). + Msg("failed to read input file") return } secretValue = string(inputFileBytes) @@ -137,22 +151,30 @@ var secretsStoreCmd = &cobra.Command{ // make sure we have valid JSON with "username" and "password" properties if !isValidCredsJSON(secretValue) { - log.Error().Err(err).Msg("not a valid JSON or creds") + log.Error(). + Err(err). + Msg("invalid JSON value or creds") os.Exit(1) } store, err = secrets.OpenStore(secretsFile) if err != nil { - fmt.Println(err) - log.Error().Err(err).Msg("failed to open secret store") + log.Error(). + Err(err). + Str("path", secretsFile). + Msg("failed to open secret store") os.Exit(1) } default: - log.Error().Msg("no input format set") + log.Error().Msg("invalid format (see --format flag for options)") os.Exit(1) } if err := store.StoreSecretByID(secretID, secretValue); err != nil { - log.Error().Err(err).Msg("failed to store secret by ID") + log.Error(). + Err(err). + Str("id", secretID). + Str("path", secretsFile). + Msg("failed to store secret by ID") os.Exit(1) } }, @@ -224,7 +246,7 @@ var secretsListCmd = &cobra.Command{ } var secretsRemoveCmd = &cobra.Command{ - Use: "remove secretIDs...", + Use: "remove secret_ids...", Args: cobra.MinimumNArgs(1), Short: "Remove secrets by IDs from secret store.", Run: func(cmd *cobra.Command, args []string) { @@ -232,21 +254,32 @@ var secretsRemoveCmd = &cobra.Command{ // open secret store from file store, err := secrets.OpenStore(secretsFile) if err != nil { - fmt.Println(err) - os.Exit(1) + log.Error(). + Err(err). + Str("path", secretsFile). + Msg("failed to open secret store") + return } // remove secret from store by it's ID err = store.RemoveSecretByID(secretID) if err != nil { - fmt.Println("failed to remove secret: ", err) - os.Exit(1) + log.Error(). + Err(err). + Str("id", secretID). + Str("path", secretsFile). + Msg("failed to remove secret") + return } // update store by saving to original file err = secrets.SaveSecrets(secretsFile, store.(*secrets.LocalSecretStore).Secrets) if err != nil { - log.Error().Err(err).Str("path", secretsFile).Msg("failed to save secrets to file") + log.Error(). + Err(err). + Str("path", secretsFile). + Msg("failed to save secrets to file") + return } } }, diff --git a/pkg/scan.go b/pkg/scan.go index 5cd6f99c..195b3825 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -106,9 +106,7 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { foundAssets, err := rawConnect(host, params.Protocol, params.Timeout, true) // if we failed to connect, exit from the function if err != nil { - if params.Verbose { - log.Debug().Err(err).Msgf("failed to connect to host") - } + log.Trace().Err(err).Msgf("failed to connect to host") continue } if !params.DisableProbing { @@ -140,6 +138,11 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { Msg("adding found asset to results after probing") break // Found a valid service, no need to probe other types + } else if err != nil { + log.Error(). + Err(err). + Str("url", probeURL). + Msg("failed to perform request") } if res != nil { if err := res.Body.Close(); err != nil { From d99cf5f234348cc0f26a3f527e66cb0fd1364e4c Mon Sep 17 00:00:00 2001 From: David Allen Date: Wed, 8 Oct 2025 12:35:37 -0600 Subject: [PATCH 05/15] fix: issue with secrets file closing too early Signed-off-by: David J. Allen Signed-off-by: David Allen --- pkg/secrets/localstore.go | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/pkg/secrets/localstore.go b/pkg/secrets/localstore.go index b021f671..0cbfde1b 100644 --- a/pkg/secrets/localstore.go +++ b/pkg/secrets/localstore.go @@ -24,7 +24,7 @@ func NewLocalSecretStore(masterKeyHex, filename string, create bool) (*LocalSecr masterKey, err := hex.DecodeString(masterKeyHex) if err != nil { - return nil, fmt.Errorf("unable to generate masterkey from hex representation: %v", err) + return nil, fmt.Errorf("failed to generate masterkey from hex representation: %v", err) } if _, err := os.Stat(filename); os.IsNotExist(err) { @@ -33,7 +33,7 @@ func NewLocalSecretStore(masterKeyHex, filename string, create bool) (*LocalSecr } file, err := os.Create(filename) if err != nil { - return nil, fmt.Errorf("unable to create file %s: %v", filename, err) + return nil, fmt.Errorf("failed to create file %s: %v", filename, err) } if err = file.Close(); err != nil { log.Warn().Err(err).Msg("could not close file") @@ -45,7 +45,7 @@ func NewLocalSecretStore(masterKeyHex, filename string, create bool) (*LocalSecr if secrets == nil { secrets, err = loadSecrets(filename) if err != nil { - return nil, fmt.Errorf("unable to load secrets from file: %v", err) + return nil, fmt.Errorf("failed to load secrets from file: %v", err) } } @@ -148,7 +148,10 @@ func SaveSecrets(jsonFile string, store map[string]string) error { encoder := json.NewEncoder(file) encoder.SetIndent("", " ") if err := file.Close(); err != nil { - log.Warn().Err(err).Msg("could not close file") + log.Warn(). + Err(err). + Str("path", jsonFile). + Msg("could not close file") } return encoder.Encode(store) } @@ -157,14 +160,23 @@ func SaveSecrets(jsonFile string, store map[string]string) error { func loadSecrets(jsonFile string) (map[string]string, error) { file, err := os.Open(jsonFile) if err != nil { - return nil, fmt.Errorf("unable to open secret file %s:%v", jsonFile, err) + return nil, fmt.Errorf("failed to open secret file %s:%v", jsonFile, err) } + defer func() { + if err := file.Close(); err != nil { + log.Warn(). + Err(err). + Str("path", jsonFile). + Msg("could not close file") + } + }() + store := make(map[string]string) decoder := json.NewDecoder(file) - if err = file.Close(); err != nil { - log.Warn().Err(err).Msg("could not close file") - } err = decoder.Decode(&store) + if err != nil { + return nil, fmt.Errorf("failed to decode file: %v", err) + } return store, err } From af5374075f943b8eae0f95a0a6a0ef7cbd804127 Mon Sep 17 00:00:00 2001 From: David Allen Date: Wed, 8 Oct 2025 12:40:54 -0600 Subject: [PATCH 06/15] chore: updated changelog Signed-off-by: David J. Allen Signed-off-by: David Allen --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2453a64..7f4ed0c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.5.2 + +- Fixed issue with secrets file being closed too early +- Added documentation about using `--insecure` with `scan` +- Improved overall logging messages and consistency + ## 0.5.1 ### Added From d554dac8c48bcc37f033a85ccd6c79f1f769f68e Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 13 Oct 2025 11:25:49 -0600 Subject: [PATCH 07/15] refactor: removed unused flags and added --insecure to collect Signed-off-by: David J. Allen Signed-off-by: David Allen --- cmd/collect.go | 7 ++----- pkg/collect.go | 12 ++---------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/cmd/collect.go b/cmd/collect.go index 5a24a53d..99548847 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -117,12 +117,10 @@ var CollectCmd = &cobra.Command{ params := &magellan.CollectParams{ Timeout: timeout, Concurrency: concurrency, - CaCertPath: cacertPath, OutputPath: outputPath, OutputDir: outputDir, + Insecure: insecure, Format: collectOutputFormat, - ForceUpdate: forceUpdate, - AccessToken: accessToken, SecretStore: store, BMCIDMap: idMap, } @@ -153,9 +151,8 @@ func init() { CollectCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the protocol used to query") CollectCmd.Flags().StringVarP(&outputPath, "output-file", "o", "", "Set the path to store collection data in a single file") CollectCmd.Flags().StringVarP(&outputDir, "output-dir", "O", "", "Set the path to store collection data using HIVE partitioning") + CollectCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Skip TLS certificate verification during probe") CollectCmd.Flags().BoolVar(&showOutput, "show", false, "Show the output of a collect run") - 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().VarP(&collectOutputFormat, "format", "F", "Set the default output data format (json|yaml; can be overridden by file extensions)") CollectCmd.Flags().StringVarP(&idMap, "bmc-id-map", "m", "", "Set the BMC ID mapping from raw json data or use @ to specify a file path (json or yaml input)") diff --git a/pkg/collect.go b/pkg/collect.go index e932375b..3ccb8a5d 100644 --- a/pkg/collect.go +++ b/pkg/collect.go @@ -14,7 +14,6 @@ import ( "github.com/OpenCHAMI/magellan/internal/format" "github.com/OpenCHAMI/magellan/internal/util" "github.com/OpenCHAMI/magellan/pkg/bmc" - "github.com/OpenCHAMI/magellan/pkg/client" "github.com/OpenCHAMI/magellan/pkg/crawler" "github.com/OpenCHAMI/magellan/pkg/idmap" "github.com/OpenCHAMI/magellan/pkg/secrets" @@ -32,12 +31,10 @@ import ( type CollectParams struct { Concurrency int // set the of concurrent jobs with the 'concurrency' flag Timeout int // set the timeout with the 'timeout' flag - CaCertPath string // set the cert path with the 'cacert' flag + Insecure bool // set whether to ignore TLS verification OutputPath string // set the path to save output with 'output' flag OutputDir string // set the directory path to save output with `output-dir` flag Format format.DataFormat // set the output format - 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 BMCIDMap string // Set the path to the BMC ID mapping YAML or JSON data or file name (if any) SecretStore secrets.SecretStore // set BMC credentials } @@ -103,7 +100,7 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin config = crawler.CrawlerConfig{ URI: uri, CredentialStore: params.SecretStore, - Insecure: true, + Insecure: params.Insecure, UseDefault: true, } ) @@ -159,11 +156,6 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin data["MACAddr"] = mac } - // create and set headers for request - headers := client.HTTPHeader{} - headers.Authorization(params.AccessToken) - headers.ContentType("application/json") - // add data output to collections collection = append(collection, data) From 6c0618583c6f14559f81d547dd2e4606b6e36def Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 13 Oct 2025 11:26:01 -0600 Subject: [PATCH 08/15] chore: updated go deps Signed-off-by: David J. Allen Signed-off-by: David Allen --- go.mod | 4 +++- go.sum | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ecc303b5..aaeb9977 100644 --- a/go.mod +++ b/go.mod @@ -18,9 +18,9 @@ require ( ) require ( - dario.cat/mergo v1.0.2 github.com/Cray-HPE/hms-xname v1.4.0 github.com/rs/zerolog v1.33.0 + github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.32.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -32,6 +32,7 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/goccy/go-json v0.10.2 // indirect @@ -46,6 +47,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect diff --git a/go.sum b/go.sum index 51b6722d..edef927f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 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.4.0 h1:i47YmE8rbSfJ64simKCCC6ZVcGid3rDIX6/jfVbISAM= From 33431bca7856ddb4d86a7dd904faa4f791edd90f Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 13 Oct 2025 11:26:42 -0600 Subject: [PATCH 09/15] refactor: added precondition for scan implementation Signed-off-by: David J. Allen Signed-off-by: David Allen --- pkg/scan.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/scan.go b/pkg/scan.go index 195b3825..f466f5e5 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -25,6 +25,13 @@ type RemoteAsset struct { ServiceType string `json:"service_type,omitempty"` } +type ScanType int + +const ( + BMC ScanType = iota + PDU +) + func (ra *RemoteAsset) String() string { return fmt.Sprintf("%v %s %s %s", ra.Timestamp, @@ -42,8 +49,6 @@ type ScanParams struct { Concurrency int Timeout int DisableProbing bool - Verbose bool - Debug bool Insecure bool Include []string } @@ -70,6 +75,10 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { chanHosts = make(chan []string, params.Concurrency+1) ) + if len(params.TargetHosts) == 0 { + return []RemoteAsset{} + } + log.Trace().Any("hosts", params.TargetHosts).Msg("starting scan...") probesToRun := []struct { From 8291ccbfa2f7d43108b3b8fda68f32fa52ed2d8a Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 13 Oct 2025 11:27:12 -0600 Subject: [PATCH 10/15] refactor: changed how files are closed Signed-off-by: David J. Allen Signed-off-by: David Allen --- pkg/collect_test.go | 88 ++++++++++++ pkg/crawl_test.go | 58 ++++++++ pkg/scan_test.go | 288 ++++++++++++++++++++++++++++++++++++++ pkg/secrets/localstore.go | 24 ++-- 4 files changed, 447 insertions(+), 11 deletions(-) create mode 100644 pkg/collect_test.go create mode 100644 pkg/crawl_test.go create mode 100644 pkg/scan_test.go diff --git a/pkg/collect_test.go b/pkg/collect_test.go new file mode 100644 index 00000000..e584d473 --- /dev/null +++ b/pkg/collect_test.go @@ -0,0 +1,88 @@ +package magellan + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +type CollectTestClient struct { + assets *[]RemoteAsset + params *CollectParams +} + +func NewCollectTestClient() *CollectTestClient { + return &CollectTestClient{} +} + +func (c *CollectTestClient) PerformCollect() ([]map[string]any, error) { + return CollectInventory(c.assets, c.params) +} + +func TestCollect(t *testing.T) { + t.Parallel() + cases := []struct { + name string + assets []RemoteAsset + params *CollectParams + want int + }{ + { + name: "basic", + assets: []RemoteAsset{ + RemoteAsset{ + Host: "", + Port: 443, + }, + }, + params: &CollectParams{ + Concurrency: 1, + Timeout: timeout, + }, + want: 0, + }, + { + name: "", + }, + { + name: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + + // create mock Redfish servers + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "" { + + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("")) + })) + defer mockServer.Close() + + var ( + collection []map[string]any + err error + ) + + c := NewCollectTestClient() + c.params = tc.params + + // add mock Redfish services + // for _, mockServer := range servers { + // c.assets = append(c.assets) + // } + + collection, err = c.PerformCollect() + + assert.Error(t, nil, err) + assert.Len(t, collection, tc.want) + + }) + } +} diff --git a/pkg/crawl_test.go b/pkg/crawl_test.go new file mode 100644 index 00000000..efc9d23d --- /dev/null +++ b/pkg/crawl_test.go @@ -0,0 +1,58 @@ +package magellan + +import ( + "testing" + + "github.com/OpenCHAMI/magellan/pkg/crawler" +) + +type CrawlTestClient struct { + config *crawler.CrawlerConfig +} + +func NewCrawlTestClient() *CrawlTestClient { + return &CrawlTestClient{} +} + +func TestCrawl(t *testing.T) { + t.Parallel() + // TODO: initialize secret store for test case + + cases := []struct { + name string + config *crawler.CrawlerConfig + }{ + { + name: "basic", + config: &crawler.CrawlerConfig{ + URI: "", + Insecure: true, + CredentialStore: nil, + UseDefault: true, + }, + }, + { + name: "secrets", + config: &crawler.CrawlerConfig{ + URI: "", + Insecure: true, + CredentialStore: nil, + UseDefault: true, + }, + }, + { + name: "no_default", + config: &crawler.CrawlerConfig{ + URI: "", + Insecure: true, + CredentialStore: nil, + UseDefault: false, + }, + }, + } + + for _, tc := range cases { + c := NewCrawlTestClient() + c.config = tc.config + } +} diff --git a/pkg/scan_test.go b/pkg/scan_test.go new file mode 100644 index 00000000..f928a0f8 --- /dev/null +++ b/pkg/scan_test.go @@ -0,0 +1,288 @@ +package magellan + +import ( + "fmt" + "math" + "net" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + defaultBaseURI = "https://bmc.openchami.cluster" + scheme = "https" + protocol = "tcp" + timeout = 10 + serviceRootResponse = `{ + "@odata.etag": "W/\"1646860561\"", + "@odata.id": "/redfish/v1/", + "@odata.type": "#ServiceRoot.v1_2_0.ServiceRoot", + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "CertificateService": { + "@odata.id": "/redfish/v1/CertificateService" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "Description": "The service root for all Redfish requests on this host", + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "Id": "RootService", + "JsonSchemas": { + "@odata.id": "/redfish/v1/JsonSchemas" + }, + "Links": { + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Name": "Root Service", + "RedfishVersion": "1.2.0", + "Registries": { + "@odata.id": "/redfish/v1/Registries" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService" + } +}` +) + +type ScanTestClient struct { + params *ScanParams +} + +func NewScanTestClient() *ScanTestClient { + return &ScanTestClient{} +} + +func ServiceRoot(baseURI string) string { + return fmt.Sprintf("%s/redfish/v1", baseURI) +} + +func (c *ScanTestClient) PerformScan() []RemoteAsset { + return ScanForAssets(c.params) +} + +func TestScan(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + params *ScanParams + wantFound int + }{ + { + name: "no_hosts_found", + params: &ScanParams{ + Scheme: scheme, + Protocol: protocol, + Concurrency: 1, + Timeout: timeout, + DisableProbing: false, + Insecure: true, + Include: []string{ + "bmcs", + }, + }, + wantFound: 0, + }, + { + name: "single_host", + params: &ScanParams{ + Scheme: scheme, + Protocol: protocol, + Concurrency: 1, + Timeout: timeout, + DisableProbing: false, + Insecure: true, + Include: []string{ + "bmcs", + }, + }, + wantFound: 1, + }, + { + name: "multiple_hosts", + params: &ScanParams{ + Scheme: scheme, + Protocol: protocol, + Concurrency: 1, + Timeout: timeout, + DisableProbing: false, + Insecure: true, + Include: []string{ + "bmcs", + }, + }, + }, + { + name: "subnet_scan", + params: &ScanParams{ + Scheme: scheme, + Protocol: protocol, + Concurrency: 1, + Timeout: timeout, + DisableProbing: false, + Insecure: true, + Include: []string{ + "bmcs", + }, + }, + wantFound: 5, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Create a mock server + var servers []*httptest.Server + for range tc.wantFound { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/redfish/v1/" { + t.Fatalf("Expected to request '%s', got: %s", ServiceRoot("/"), r.URL.Path) + } + if r.Method != http.MethodGet { + t.Fatalf("Expected GET request, got: %s", r.Method) + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(serviceRootResponse)) + })) + defer mockServer.Close() // Close the server when the test finishes + servers = append(servers, mockServer) + } + + c := NewScanTestClient() + c.params = tc.params + + // add target hosts from mock servers created + for _, mockServer := range servers { + c.params.TargetHosts = append(c.params.TargetHosts, []string{mockServer.URL}) + } + + // set the number of expected responses + tc.wantFound = len(servers) + + found := c.PerformScan() + + assert.Len(t, found, tc.wantFound) + }) + + } + +} + +func TestGenerateHostsFromSubnet(t *testing.T) { + t.Parallel() + + var ( + defaultSubnetMask = net.IPMask{255, 255, 255, 0} + defaultPorts = []int{443} + ) + + type TestCase struct { + name string + subnet string + subnetMask *net.IPMask + ports []int + scheme string + wantTotalHosts int + } + + var getExpectedServiceCount = func(tc TestCase) int { + v, err := strconv.ParseInt(tc.subnetMask.String(), 16, 0) + if err != nil { + return -1 + } + return ((int(math.Pow(2, 32)) - int(v)) * len(tc.ports)) - 1 + } + + cases := []TestCase{ + { + name: "basic", + subnet: "172.21.0.0", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: scheme, + wantTotalHosts: 0, + }, + { + name: "none", + subnet: "10.0.0.0", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: scheme, + wantTotalHosts: 0, + }, + { + name: "invalid subnet", + subnet: "invalid", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: scheme, + wantTotalHosts: 0, + }, + { + name: "no subnet", + subnet: "", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: scheme, + wantTotalHosts: 0, + }, + { + name: "cidr", + subnet: "172.21.0.0/24", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: scheme, + wantTotalHosts: 0, + }, + { + name: "additional ports", + subnet: "172.21.0.0/24", + subnetMask: &defaultSubnetMask, + ports: []int{443, 5000}, + scheme: scheme, + wantTotalHosts: 0, + }, + { + name: "different scheme", + subnet: "172.21.0.0/24", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: "http", + wantTotalHosts: 0, + }, + } + + for _, tc := range cases { + tc.wantTotalHosts = getExpectedServiceCount(tc) + hosts := GenerateHostsWithSubnet( + tc.subnet, + tc.subnetMask, + tc.ports, + tc.scheme, + ) + + assert.Len(t, hosts, tc.wantTotalHosts) + } +} diff --git a/pkg/secrets/localstore.go b/pkg/secrets/localstore.go index 0cbfde1b..619bb652 100644 --- a/pkg/secrets/localstore.go +++ b/pkg/secrets/localstore.go @@ -35,9 +35,11 @@ func NewLocalSecretStore(masterKeyHex, filename string, create bool) (*LocalSecr if err != nil { return nil, fmt.Errorf("failed to create file %s: %v", filename, err) } - if err = file.Close(); err != nil { - log.Warn().Err(err).Msg("could not close file") - } + defer func() { + if err = file.Close(); err != nil { + log.Warn().Err(err).Msg("could not close file") + } + }() secrets = make(map[string]string) } @@ -144,15 +146,16 @@ func SaveSecrets(jsonFile string, store map[string]string) error { if err != nil { return err } - + defer func() { + if err := file.Close(); err != nil { + log.Warn(). + Err(err). + Str("path", jsonFile). + Msg("could not close file") + } + }() encoder := json.NewEncoder(file) encoder.SetIndent("", " ") - if err := file.Close(); err != nil { - log.Warn(). - Err(err). - Str("path", jsonFile). - Msg("could not close file") - } return encoder.Encode(store) } @@ -162,7 +165,6 @@ func loadSecrets(jsonFile string) (map[string]string, error) { if err != nil { return nil, fmt.Errorf("failed to open secret file %s:%v", jsonFile, err) } - defer func() { if err := file.Close(); err != nil { log.Warn(). From c8c5c39ccc3dc60b1bd0ce240c3ecbc836369809 Mon Sep 17 00:00:00 2001 From: David Allen Date: Mon, 13 Oct 2025 20:07:51 -0600 Subject: [PATCH 11/15] refactor: updated test implementations Signed-off-by: David J. Allen Signed-off-by: David Allen --- pkg/collect_test.go | 26 +++++++- pkg/crawl_test.go | 51 +++++++++------- pkg/scan.go | 14 +++-- pkg/scan_test.go | 137 +++++++++++++----------------------------- pkg/test/constants.go | 130 +++++++++++++++++++++++++++++++++++++++ pkg/test/test.go | 20 ++++++ 6 files changed, 253 insertions(+), 125 deletions(-) create mode 100644 pkg/test/constants.go create mode 100644 pkg/test/test.go diff --git a/pkg/collect_test.go b/pkg/collect_test.go index e584d473..73b51f31 100644 --- a/pkg/collect_test.go +++ b/pkg/collect_test.go @@ -5,6 +5,7 @@ import ( "net/http/httptest" "testing" + "github.com/OpenCHAMI/magellan/internal/format" "github.com/stretchr/testify/assert" ) @@ -32,22 +33,41 @@ func TestCollect(t *testing.T) { { name: "basic", assets: []RemoteAsset{ - RemoteAsset{ - Host: "", - Port: 443, + { + Host: "", + Port: 443, + State: true, + Protocol: "tcp", + ServiceType: BMC, }, }, params: &CollectParams{ Concurrency: 1, Timeout: timeout, + Insecure: true, + Format: format.FORMAT_JSON, + SecretStore: nil, }, want: 0, }, { name: "", + assets: []RemoteAsset{ + { + Host: "", + Port: 443, + State: true, + Protocol: "tcp", + ServiceType: BMC, + }, + }, + params: &CollectParams{}, }, { name: "", + assets: []RemoteAsset{ + {}, + }, }, } diff --git a/pkg/crawl_test.go b/pkg/crawl_test.go index efc9d23d..1cbca9b8 100644 --- a/pkg/crawl_test.go +++ b/pkg/crawl_test.go @@ -1,9 +1,13 @@ package magellan import ( + "net/http/httptest" "testing" "github.com/OpenCHAMI/magellan/pkg/crawler" + "github.com/OpenCHAMI/magellan/pkg/secrets" + "github.com/OpenCHAMI/magellan/pkg/test" + "github.com/go-chi/chi/v5" ) type CrawlTestClient struct { @@ -11,12 +15,18 @@ type CrawlTestClient struct { } func NewCrawlTestClient() *CrawlTestClient { - return &CrawlTestClient{} + store := secrets.NewStaticStore("test", "test") + return &CrawlTestClient{ + config: &crawler.CrawlerConfig{ + URI: "http://localhost:5000", + Insecure: true, + CredentialStore: store, + }, + } } func TestCrawl(t *testing.T) { t.Parallel() - // TODO: initialize secret store for test case cases := []struct { name string @@ -25,34 +35,33 @@ func TestCrawl(t *testing.T) { { name: "basic", config: &crawler.CrawlerConfig{ - URI: "", - Insecure: true, - CredentialStore: nil, - UseDefault: true, - }, - }, - { - name: "secrets", - config: &crawler.CrawlerConfig{ - URI: "", - Insecure: true, - CredentialStore: nil, - UseDefault: true, + UseDefault: true, }, }, { name: "no_default", config: &crawler.CrawlerConfig{ - URI: "", - Insecure: true, - CredentialStore: nil, - UseDefault: false, + UseDefault: false, }, }, } for _, tc := range cases { - c := NewCrawlTestClient() - c.config = tc.config + t.Run(tc.name, func(t *testing.T) { + mux := chi.NewMux() + mux.HandleFunc("/redfish/v1", test.Make(test.RESPONSE_ServiceRoot)) + mux.HandleFunc("/redfish/v1/Systems", test.Make(test.RESPONSE_Systems)) + mux.HandleFunc("/redfish/v1/Node0/EthernetInterfaces", test.Make(test.RESPONSE_EthernetInterface)) + + // create a mock server to simulator a Redfish service + mockServer := httptest.NewServer(mux) + defer mockServer.Close() + + c := NewCrawlTestClient() + c.config = tc.config + + crawler.CrawlBMCForSystems(*c.config) + crawler.CrawlBMCForManagers(*c.config) + }) } } diff --git a/pkg/scan.go b/pkg/scan.go index f466f5e5..1501ec61 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -22,16 +22,20 @@ type RemoteAsset struct { Protocol string `json:"protocol"` State bool `json:"state"` Timestamp time.Time `json:"timestamp"` - ServiceType string `json:"service_type,omitempty"` + ServiceType Scanner `json:"service_type,omitempty"` } -type ScanType int +type Scanner string const ( - BMC ScanType = iota - PDU + BMC Scanner = "bmcs" + PDU Scanner = "pdus" ) +func (st Scanner) String() string { + return string(st) +} + func (ra *RemoteAsset) String() string { return fmt.Sprintf("%v %s %s %s", ra.Timestamp, @@ -140,7 +144,7 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { Str("url", probeURL). Msg("could not close response resource") } - foundAsset.ServiceType = probe.Type + foundAsset.ServiceType = Scanner(probe.Type) assetsToAdd = append(assetsToAdd, foundAsset) log.Debug(). Str("host", foundAsset.Host). diff --git a/pkg/scan_test.go b/pkg/scan_test.go index f928a0f8..260eb44d 100644 --- a/pkg/scan_test.go +++ b/pkg/scan_test.go @@ -9,61 +9,14 @@ import ( "strconv" "testing" + "github.com/OpenCHAMI/magellan/pkg/test" "github.com/stretchr/testify/assert" ) const ( - defaultBaseURI = "https://bmc.openchami.cluster" - scheme = "https" - protocol = "tcp" - timeout = 10 - serviceRootResponse = `{ - "@odata.etag": "W/\"1646860561\"", - "@odata.id": "/redfish/v1/", - "@odata.type": "#ServiceRoot.v1_2_0.ServiceRoot", - "AccountService": { - "@odata.id": "/redfish/v1/AccountService" - }, - "CertificateService": { - "@odata.id": "/redfish/v1/CertificateService" - }, - "Chassis": { - "@odata.id": "/redfish/v1/Chassis" - }, - "Description": "The service root for all Redfish requests on this host", - "EventService": { - "@odata.id": "/redfish/v1/EventService" - }, - "Id": "RootService", - "JsonSchemas": { - "@odata.id": "/redfish/v1/JsonSchemas" - }, - "Links": { - "Sessions": { - "@odata.id": "/redfish/v1/SessionService/Sessions" - } - }, - "Managers": { - "@odata.id": "/redfish/v1/Managers" - }, - "Name": "Root Service", - "RedfishVersion": "1.2.0", - "Registries": { - "@odata.id": "/redfish/v1/Registries" - }, - "SessionService": { - "@odata.id": "/redfish/v1/SessionService" - }, - "Systems": { - "@odata.id": "/redfish/v1/Systems" - }, - "Tasks": { - "@odata.id": "/redfish/v1/TaskService" - }, - "UpdateService": { - "@odata.id": "/redfish/v1/UpdateService" - } -}` + scheme = "https" + protocol = "tcp" + timeout = 10 ) type ScanTestClient struct { @@ -153,6 +106,7 @@ func TestScan(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + // Create a mock server var servers []*httptest.Server for range tc.wantFound { @@ -164,7 +118,7 @@ func TestScan(t *testing.T) { t.Fatalf("Expected GET request, got: %s", r.Method) } w.WriteHeader(http.StatusOK) - w.Write([]byte(serviceRootResponse)) + w.Write([]byte(test.RESPONSE_ServiceRoot)) })) defer mockServer.Close() // Close the server when the test finishes servers = append(servers, mockServer) @@ -185,9 +139,7 @@ func TestScan(t *testing.T) { assert.Len(t, found, tc.wantFound) }) - } - } func TestGenerateHostsFromSubnet(t *testing.T) { @@ -217,60 +169,53 @@ func TestGenerateHostsFromSubnet(t *testing.T) { cases := []TestCase{ { - name: "basic", - subnet: "172.21.0.0", - subnetMask: &defaultSubnetMask, - ports: defaultPorts, - scheme: scheme, - wantTotalHosts: 0, + name: "basic", + subnet: "172.21.0.0", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: scheme, }, { - name: "none", - subnet: "10.0.0.0", - subnetMask: &defaultSubnetMask, - ports: defaultPorts, - scheme: scheme, - wantTotalHosts: 0, + name: "none", + subnet: "10.0.0.0", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: scheme, }, { - name: "invalid subnet", - subnet: "invalid", - subnetMask: &defaultSubnetMask, - ports: defaultPorts, - scheme: scheme, - wantTotalHosts: 0, + name: "invalid subnet", + subnet: "invalid", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: scheme, }, { - name: "no subnet", - subnet: "", - subnetMask: &defaultSubnetMask, - ports: defaultPorts, - scheme: scheme, - wantTotalHosts: 0, + name: "no subnet", + subnet: "", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: scheme, }, { - name: "cidr", - subnet: "172.21.0.0/24", - subnetMask: &defaultSubnetMask, - ports: defaultPorts, - scheme: scheme, - wantTotalHosts: 0, + name: "cidr", + subnet: "172.21.0.0/24", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: scheme, }, { - name: "additional ports", - subnet: "172.21.0.0/24", - subnetMask: &defaultSubnetMask, - ports: []int{443, 5000}, - scheme: scheme, - wantTotalHosts: 0, + name: "additional ports", + subnet: "172.21.0.0/24", + subnetMask: &defaultSubnetMask, + ports: []int{443, 5000}, + scheme: scheme, }, { - name: "different scheme", - subnet: "172.21.0.0/24", - subnetMask: &defaultSubnetMask, - ports: defaultPorts, - scheme: "http", - wantTotalHosts: 0, + name: "different scheme", + subnet: "172.21.0.0/24", + subnetMask: &defaultSubnetMask, + ports: defaultPorts, + scheme: "http", }, } diff --git a/pkg/test/constants.go b/pkg/test/constants.go new file mode 100644 index 00000000..7c4ad4c9 --- /dev/null +++ b/pkg/test/constants.go @@ -0,0 +1,130 @@ +package test + +const ( + RESPONSE_ServiceRoot = `{ + "@odata.etag": "W/\"1646860561\"", + "@odata.id": "/redfish/v1/", + "@odata.type": "#ServiceRoot.v1_2_0.ServiceRoot", + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "CertificateService": { + "@odata.id": "/redfish/v1/CertificateService" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "Description": "The service root for all Redfish requests on this host", + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "Id": "RootService", + "JsonSchemas": { + "@odata.id": "/redfish/v1/JsonSchemas" + }, + "Links": { + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Name": "Root Service", + "RedfishVersion": "1.2.0", + "Registries": { + "@odata.id": "/redfish/v1/Registries" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService" + } + }` + RESPONSE_EthernetInterface = `{ + "@odata.etag": "W/\"1646792654\"", + "@odata.id": "/redfish/v1/Systems/Node0", + "@odata.type": "#ComputerSystem.v1_5_0.ComputerSystem", + "Actions": { + "#ComputerSystem.Reset": { + "@Redfish.ActionInfo": "/redfish/v1/Systems/Node0/ResetActionInfo", + "target": "/redfish/v1/Systems/Node0/Actions/ComputerSystem.Reset" + }, + "#ComputerSystem.SetDefaultBootOrder": { + "@Redfish.ActionInfo": "/redfish/v1/Systems/Node0/SetDefaultBootOrderActionInfo", + "target": "/redfish/v1/Systems/Node0/Actions/ComputerSystem.SetDefaultBootOrder" + } + }, + "Bios": { + "@odata.id": "/redfish/v1/Systems/Node0/Bios" + }, + "BiosVersion": "ex235a.bios-1.3.6", + "Boot": { + "BootOptions": { + "@odata.id": "/redfish/v1/Systems/Node0/BootOptions" + }, + "BootOrder": [ + "ME0-PXE-IP4", + "ME0-PXE-IP6", + "HSN0-PXE-IP4", + "HSN0-PXE-IP6", + "HSN1-PXE-IP4", + "HSN1-PXE-IP6", + "HSN2-PXE-IP4", + "HSN3-PXE-IP4", + "HSN2-PXE-IP6", + "HSN3-PXE-IP6", + "Boot000B" + ] + }, + "Description": "BardPeakNC", + "EthernetInterfaces": { + "@odata.id": "/redfish/v1/Systems/Node0/EthernetInterfaces" + }, + "Id": "Node0", + "Manufacturer": "HPE", + "Memory": { + "@odata.id": "/redfish/v1/Systems/Node0/Memory" + }, + "MemorySummary": { + "TotalSystemMemoryGiB": 512 + }, + "Model": "HPE CRAY EX235a", + "Name": "Node0", + "PartNumber": "P37085-001.A", + "PowerState": "On", + "ProcessorSummary": { + "Count": 9, + "Model": "AMD INSTINCT MI200 (MCM) OAM LC" + }, + "Processors": { + "@odata.id": "/redfish/v1/Systems/Node0/Processors" + }, + "SerialNumber": "GHU4464825942", + "Status": { + "Health": "OK", + "State": "Enabled" + }, + "SystemType": "Physical" +}` + RESPONSE_Systems = `{ + "@odata.etag": "W/\"1646792654\"", + "@odata.id": "/redfish/v1/Systems", + "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", + "Description": "Collection of Computer Systems", + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/Node0" + } + ], + "Members@odata.count": 1, + "Name": "Systems Collection" +}` +) diff --git a/pkg/test/test.go b/pkg/test/test.go new file mode 100644 index 00000000..2f56fc00 --- /dev/null +++ b/pkg/test/test.go @@ -0,0 +1,20 @@ +package test + +import ( + "net/http" +) + +func Make(response string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // if r.URL.Path != path { + // http.Error( + // w, + // fmt.Sprintf("expected path '%s' but got '%s' instead", path, r.URL.Path), + // http.StatusInternalServerError, + // ) + // return + // } + w.WriteHeader(http.StatusOK) + w.Write([]byte(response)) + } +} From 1fc9d8d0ec725ad0ba47e639c9e7a71ce521f585 Mon Sep 17 00:00:00 2001 From: David Allen Date: Tue, 28 Oct 2025 15:09:22 -0600 Subject: [PATCH 12/15] refactor: minor formatting changes Signed-off-by: David J. Allen Signed-off-by: David Allen --- cmd/collect.go | 4 ++-- cmd/send.go | 8 ++++++-- pkg/crawler/main.go | 11 ++++++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cmd/collect.go b/cmd/collect.go index 99548847..6fd1bbd5 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -165,8 +165,8 @@ func init() { checkBindFlagError(viper.BindPFlag("collect.protocol", CollectCmd.Flags().Lookup("protocol"))) 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.BindPFlag("collect.force-update", CollectCmd.Flags().Lookup("force-update"))) + // checkBindFlagError(viper.BindPFlag("collect.cacert", CollectCmd.Flags().Lookup("cacert"))) checkBindFlagError(viper.BindPFlags(CollectCmd.Flags())) rootCmd.AddCommand(CollectCmd) diff --git a/cmd/send.go b/cmd/send.go index 8e161606..ee37265c 100644 --- a/cmd/send.go +++ b/cmd/send.go @@ -110,10 +110,14 @@ var sendCmd = &cobra.Command{ 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) + 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) + log.Error(). + Err(err). + Msgf("failed to add Redfish endpoint with ID %s", smdClient.Xname) } } } diff --git a/pkg/crawler/main.go b/pkg/crawler/main.go index 0bca4708..85285961 100644 --- a/pkg/crawler/main.go +++ b/pkg/crawler/main.go @@ -238,11 +238,14 @@ func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) { // Obtain the ServiceRoot rf_service := client.GetService() - log.Debug().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() if err != nil { - log.Error().Err(err).Msg("failed to get managers from ServiceRoot") + log.Error(). + Err(err). + Msg("failed to get managers from ServiceRoot") } return walkManagers(rf_managers, config.URI) } @@ -281,7 +284,9 @@ func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chass // get all of the links to managers rf_managers, err := rf_computersystem.ManagedBy() if err != nil { - log.Warn().Err(err).Msg("failed to get system managers") + log.Warn(). + Err(err). + Msg("failed to get system managers") log.Error(). Err(err). Str("id", rf_computersystem.ID). From ec94740fcd7cebb7ee180955fe1659b9d9f3248d Mon Sep 17 00:00:00 2001 From: David Allen Date: Wed, 29 Oct 2025 06:52:57 -0600 Subject: [PATCH 13/15] refactor: minor changes to debug messages Signed-off-by: David J. Allen Signed-off-by: David Allen --- cmd/send.go | 23 ++++++++++++++++++----- pkg/client/smd.go | 7 ++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/cmd/send.go b/cmd/send.go index ee37265c..03452104 100644 --- a/cmd/send.go +++ b/cmd/send.go @@ -71,7 +71,8 @@ var sendCmd = &cobra.Command{ } // show the data that was just loaded as input - log.Debug().Any("input", inputData).Send() + // inputRaw, _ := json.MarshalIndent(inputData, "", " ") + log.Debug().Int("endpoint_count", len(inputData)).Send() for _, host := range args { var ( @@ -83,6 +84,7 @@ var sendCmd = &cobra.Command{ for _, dataObject := range inputData { // skip on to the next thing if it's does not exist if dataObject == nil { + log.Warn().Str("host", host).Msg("skipping request to host") continue } @@ -93,16 +95,23 @@ var sendCmd = &cobra.Command{ host, err = urlx.Sanitize(host) if err != nil { - log.Warn().Err(err).Str("host", host).Msg("could not sanitize host") + 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") + log.Error(). + Err(err). + Msg("failed to marshal request data") continue } + log.Debug().Str("host", host).RawJSON("data", body).Send() + // make request to remote host err = smdClient.Add(body, headers) if err != nil { // try updating instead @@ -112,12 +121,16 @@ var sendCmd = &cobra.Command{ if err != nil { log.Error(). Err(err). - Msgf("failed to forcibly update Redfish endpoint with ID %s", smdClient.Xname) + Str("host", host). + Str("ID", smdClient.Xname). + Msgf("failed to forcibly update Redfish endpoint") } } else { log.Error(). Err(err). - Msgf("failed to add Redfish endpoint with ID %s", smdClient.Xname) + Str("host", host). + Str("ID", smdClient.Xname). + Msgf("failed to add Redfish endpoint") } } } diff --git a/pkg/client/smd.go b/pkg/client/smd.go index 0f2fbb23..ea0fbee3 100644 --- a/pkg/client/smd.go +++ b/pkg/client/smd.go @@ -59,7 +59,12 @@ func (c *SmdClient) Add(data HTTPBody, headers HTTPHeader) error { return fmt.Errorf("returned status code %d when adding endpoint", res.StatusCode) } } - log.Debug().Msgf("%v (%v)\n%s\n", url, res.Status, string(body)) + log.Debug(). + Str("url", url). + Str("status", res.Status). + Int("status", res.StatusCode). + RawJSON("body", body). + Send() } return err } From 633be665467393ed857d5dac53b6dfc8223e3461 Mon Sep 17 00:00:00 2001 From: David Allen Date: Thu, 5 Feb 2026 16:16:10 -0700 Subject: [PATCH 14/15] refactor: changed status code string Signed-off-by: David Allen --- pkg/client/smd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/client/smd.go b/pkg/client/smd.go index ea0fbee3..b6653ec2 100644 --- a/pkg/client/smd.go +++ b/pkg/client/smd.go @@ -62,7 +62,7 @@ func (c *SmdClient) Add(data HTTPBody, headers HTTPHeader) error { log.Debug(). Str("url", url). Str("status", res.Status). - Int("status", res.StatusCode). + Int("status_code", res.StatusCode). RawJSON("body", body). Send() } From 4d188cf4a2843ba106b766c2d4794f3b724a665e Mon Sep 17 00:00:00 2001 From: David Allen Date: Thu, 5 Feb 2026 16:16:37 -0700 Subject: [PATCH 15/15] feat: added serial console and command shell to output Signed-off-by: David Allen --- pkg/crawler/main.go | 93 +++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/pkg/crawler/main.go b/pkg/crawler/main.go index 85285961..28d519ef 100644 --- a/pkg/crawler/main.go +++ b/pkg/crawler/main.go @@ -49,14 +49,16 @@ type NetworkInterface struct { } type Manager struct { - URI string `json:"uri,omitempty"` - UUID string `json:"uuid,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Model string `json:"model,omitempty"` - Type string `json:"type,omitempty"` - FirmwareVersion string `json:"firmware_version,omitempty"` - EthernetInterfaces []EthernetInterface `json:"ethernet_interfaces,omitempty"` + URI string `json:"uri,omitempty"` + UUID string `json:"uuid,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Model string `json:"model,omitempty"` + Type string `json:"type,omitempty"` + FirmwareVersion string `json:"firmware_version,omitempty"` + EthernetInterfaces []EthernetInterface `json:"ethernet_interfaces,omitempty"` + SerialConsoleSupported []string `json:"serial_console"` + CommandShellSupported []string `json:"command_shell"` } type Links struct { @@ -71,14 +73,26 @@ type Power struct { PowerControlIDs []string `json:"power_control_ids,omitempty"` } +type SerialConsoleConfig struct { + Port int `json:"port,omitempty"` + Enabled bool `json:"enabled,omitempty"` +} + +type SerialConsole struct { + IPMI SerialConsoleConfig `json:"impi,omitempty"` + Telnet SerialConsoleConfig `json:"telnet,omitempty"` + SSH SerialConsoleConfig `json:"ssh,omitempty"` +} + type InventoryDetail struct { URI string `json:"uri,omitempty"` // URI of the BMC UUID string `json:"uuid,omitempty"` // UUID of Node Manufacturer string `json:"manufacturer,omitempty"` // Manufacturer of the Node SystemType string `json:"system_type,omitempty"` // System type of the Node Name string `json:"name,omitempty"` // Name of the Node - Model string `json:"model,omitempty"` // Model of the Node - Serial string `json:"serial,omitempty"` // Serial number of the Node + ModelNumber string `json:"model,omitempty"` // Model of the Node + SerialNumber string `json:"serial,omitempty"` // Serial number of the Node + SerialConsole SerialConsole `json:"serial_console,omitempty"` // Supported serial console types of the Node BiosVersion string `json:"bios_version,omitempty"` // Version of the BIOS EthernetInterfaces []EthernetInterface `json:"ethernet_interfaces,omitempty"` // Ethernet interfaces of the Node NetworkInterfaces []NetworkInterface `json:"network_interfaces,omitempty"` // Network interfaces of the Node @@ -204,11 +218,7 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) { } // If nodes are found under both Chassis and Systems, Systems is assumed to be "more definitive" // and will override corresponding fields from the Chassis version. - // err = mergo.Merge(&systems, newSystems, mergo.WithOverride) - systems, err = merge(systems, newSystems) - if err != nil { - return extractPtrMapValues(systems), fmt.Errorf("failed to merge systems from Chassis and Systems endpoints: %v", err) - } + systems = merge(systems, newSystems) return extractPtrMapValues(systems), nil } @@ -329,9 +339,23 @@ func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chass Name: rf_computersystem.Name, Manufacturer: rf_computersystem.Manufacturer, SystemType: string(rf_computersystem.SystemType), - Model: rf_computersystem.Model, - Serial: rf_computersystem.SerialNumber, - BiosVersion: rf_computersystem.BIOSVersion, + ModelNumber: rf_computersystem.Model, + SerialNumber: rf_computersystem.SerialNumber, + SerialConsole: SerialConsole{ + IPMI: SerialConsoleConfig{ + Port: rf_computersystem.SerialConsole.IPMI.Port, + Enabled: rf_computersystem.SerialConsole.IPMI.ServiceEnabled, + }, + SSH: SerialConsoleConfig{ + Port: rf_computersystem.SerialConsole.SSH.Port, + Enabled: rf_computersystem.SerialConsole.SSH.ServiceEnabled, + }, + Telnet: SerialConsoleConfig{ + Port: rf_computersystem.SerialConsole.Telnet.Port, + Enabled: rf_computersystem.SerialConsole.Telnet.ServiceEnabled, + }, + }, + BiosVersion: rf_computersystem.BIOSVersion, Links: Links{ Managers: managerLinks, Chassis: chassisLinks, @@ -356,6 +380,7 @@ func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chass system.Chassis_Model = rf_chassis.Model } + // add ethernet interfaces rf_ethernetinterfaces, err := rf_computersystem.EthernetInterfaces() if err != nil { log.Error().Err(err).Msg("failed to get ethernet interfaces from computer system") @@ -381,6 +406,7 @@ func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chass return systems, err } + // add network interfaces for _, rf_networkInterface := range rf_networkInterfaces { rf_networkAdapter, err := rf_networkInterface.NetworkAdapter() if err != nil { @@ -457,15 +483,26 @@ func walkManagers(rf_managers []*redfish.Manager, baseURI string) ([]Manager, er IP: rf_ethernetinterface.IPv4Addresses[0].Address, }) } + + var supported_serial_console []string + for _, console_type := range rf_manager.SerialConsole.ConnectTypesSupported { + supported_serial_console = append(supported_serial_console, string(console_type)) + } + var supported_command_shell []string + for _, shell_type := range rf_manager.CommandShell.ConnectTypesSupported { + supported_command_shell = append(supported_command_shell, string(shell_type)) + } managers = append(managers, Manager{ - URI: baseURI + "/redfish/v1/Managers/" + rf_manager.ID, - UUID: rf_manager.UUID, - Name: rf_manager.Name, - Description: rf_manager.Description, - Model: rf_manager.Model, - Type: string(rf_manager.ManagerType), - FirmwareVersion: rf_manager.FirmwareVersion, - EthernetInterfaces: ethernet_interfaces, + URI: baseURI + "/redfish/v1/Managers/" + rf_manager.ID, + UUID: rf_manager.UUID, + Name: rf_manager.Name, + Description: rf_manager.Description, + Model: rf_manager.Model, + Type: string(rf_manager.ManagerType), + FirmwareVersion: rf_manager.FirmwareVersion, + EthernetInterfaces: ethernet_interfaces, + SerialConsoleSupported: supported_serial_console, + CommandShellSupported: supported_command_shell, }) } return managers, nil @@ -528,10 +565,10 @@ func extractPtrMapValues[T any](m map[string]*T) []T { return slice } -func merge(systems map[string]*InventoryDetail, newSystems []InventoryDetail) (map[string]*InventoryDetail, error) { +func merge(systems map[string]*InventoryDetail, newSystems []InventoryDetail) map[string]*InventoryDetail { // add and replace values in systems with values from newSystems for _, system := range newSystems { systems[system.URI] = &system } - return systems, nil + return systems }