diff --git a/cmd/crawl.go b/cmd/crawl.go index 8069c9b..2611ed3 100644 --- a/cmd/crawl.go +++ b/cmd/crawl.go @@ -35,7 +35,7 @@ var crawlCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { - systems, err := crawler.CrawlBMC(crawler.CrawlerConfig{ + systems, err := crawler.CrawlBMCForSystems(crawler.CrawlerConfig{ URI: args[0], Username: cmd.Flag("username").Value.String(), Password: cmd.Flag("password").Value.String(), diff --git a/internal/collect.go b/internal/collect.go index e853802..e52bc60 100644 --- a/internal/collect.go +++ b/internal/collect.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "path" + "strings" "path/filepath" "sync" "time" @@ -21,7 +22,8 @@ import ( "github.com/Cray-HPE/hms-xname/xnames" _ "github.com/mattn/go-sqlite3" - _ "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/redfish" "golang.org/x/exp/slices" ) @@ -41,12 +43,13 @@ type CollectParams struct { } // This is the main function used to collect information from the BMC nodes via Redfish. +// The results of the collect are stored in a cache specified with the `--cache` flag. // The function expects a list of hosts found using the `ScanForAssets()` function. // // Requests can be made to several of the nodes using a goroutine by setting the q.Concurrency -// property value between 1 and 255. +// property value between 1 and 10000. func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { - // check for available probe states + // check for available remote assets found from scan if assets == nil { return fmt.Errorf("no assets found") } @@ -109,14 +112,23 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { offset += 1 // crawl BMC node to fetch inventory data via Redfish - systems, err := crawler.CrawlBMC(crawler.CrawlerConfig{ - URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port), - Username: params.Username, - Password: params.Password, - Insecure: true, - }) + var ( + systems []crawler.InventoryDetail + managers []crawler.Manager + config = crawler.CrawlerConfig{ + URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port), + Username: params.Username, + Password: params.Password, + Insecure: true, + } + ) + systems, err := crawler.CrawlBMCForSystems(config) + if err != nil { + log.Error().Err(err).Msg("failed to crawl BMC for systems") + } + managers, err = crawler.CrawlBMCForManagers(config) if err != nil { - log.Error().Err(err).Msgf("failed to crawl BMC") + log.Error().Err(err).Msg("failed to crawl BMC for managers") } // data to be sent to smd @@ -129,9 +141,20 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { "MACRequired": true, "RediscoverOnUpdate": false, "Systems": systems, + "Managers": managers, "SchemaVersion": 1, } + // optionally, add the MACAddr property if we find a matching IP + // from the correct ethernet interface + mac, err := FindMACAddressWithIP(config, net.ParseIP(sr.Host)) + if err != nil { + log.Warn().Err(err).Msgf("failed to find MAC address with IP '%s'", sr.Host) + } + if mac != "" { + data["MACAddr"] = mac + } + // create and set headers for request headers := client.HTTPHeader{} headers.Authorization(params.AccessToken) @@ -220,3 +243,75 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { return nil } + +// FindMACAddressWithIP() returns the MAC address of an ethernet interface with +// a matching IPv4Address. Returns an empty string and error if there are no matches +// found. +func FindMACAddressWithIP(config crawler.CrawlerConfig, targetIP net.IP) (string, error) { + // get the managers to find the BMC MAC address compared with IP + // + // NOTE: Since we don't have a RedfishEndpoint type abstraction in + // magellan and the crawler crawls for systems information, it + // may just make more sense to get the managers directly via + // gofish (at least for now). If there's a need for grabbing more + // manager information in the future, we can move the logic into + // the crawler. + client, err := gofish.Connect(gofish.ClientConfig{ + Endpoint: config.URI, + Username: config.Username, + Password: config.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 "", err + } + defer client.Logout() + + var ( + rf_service = client.GetService() + rf_managers []*redfish.Manager + ) + rf_managers, err = rf_service.Managers() + if err != nil { + return "", fmt.Errorf("failed to get managers: %v", err) + } + + // find the manager with the same IP address of the BMC to get + // it's MAC address from its EthernetInterface + for _, manager := range rf_managers { + eths, err := manager.EthernetInterfaces() + if err != nil { + log.Error().Err(err).Msgf("failed to get ethernet interfaces from manager '%s'", manager.Name) + continue + } + for _, eth := range eths { + // compare the ethernet interface IP with argument + for _, ip := range eth.IPv4Addresses { + if ip.Address == targetIP.String() { + // we found matching IP address so return the ethernet interface MAC + return eth.MACAddress, nil + } + } + // do the same thing as above, but with static IP addresses + for _, ip := range eth.IPv4StaticAddresses { + if ip.Address == targetIP.String() { + return eth.MACAddress, nil + } + } + // no matches found, so go to next ethernet interface + continue + } + } + // no matches found, so return an empty string + return "", fmt.Errorf("no ethernet interfaces found with IP address") +} diff --git a/pkg/crawler/main.go b/pkg/crawler/main.go index e28cb1b..771efb9 100644 --- a/pkg/crawler/main.go +++ b/pkg/crawler/main.go @@ -41,6 +41,17 @@ type NetworkInterface struct { Adapter NetworkAdapter `json:"adapter,omitempty"` // Adapter of the interface } +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"` +} + type InventoryDetail struct { URI string `json:"uri,omitempty"` // URI of the BMC UUID string `json:"uuid,omitempty"` // UUID of Node @@ -65,9 +76,12 @@ type InventoryDetail struct { Chassis_Model string `json:"chassis_model,omitempty"` // Model of the Chassis } -// CrawlBMC pulls all pertinent information from a BMC. It accepts a CrawlerConfig and returns a list of InventoryDetail structs. -func CrawlBMC(config CrawlerConfig) ([]InventoryDetail, error) { - var systems []InventoryDetail +// 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 []InventoryDetail + rf_systems []*redfish.ComputerSystem + ) // initialize gofish client client, err := gofish.Connect(gofish.ClientConfig{ Endpoint: config.URI, @@ -94,8 +108,6 @@ func CrawlBMC(config CrawlerConfig) ([]InventoryDetail, error) { rf_service := client.GetService() log.Info().Msgf("found ServiceRoot %s. Redfish Version %s", rf_service.ID, rf_service.RedfishVersion) - var rf_systems []*redfish.ComputerSystem - // Nodes are sometimes only found under Chassis, but they should be found under Systems. rf_chassis, err := rf_service.Chassis() if err == nil { @@ -114,8 +126,43 @@ func CrawlBMC(config CrawlerConfig) ([]InventoryDetail, error) { } log.Info().Msgf("found %d systems in ServiceRoot", len(rf_root_systems)) rf_systems = append(rf_systems, rf_root_systems...) - systems, err = walkSystems(rf_systems, nil, config.URI) - return systems, err + return walkSystems(rf_systems, nil, config.URI) +} + +// CrawlBMCForSystems pulls BMC manager information. +func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) { + // initialize gofish client + var managers []Manager + client, err := gofish.Connect(gofish.ClientConfig{ + Endpoint: config.URI, + Username: config.Username, + Password: config.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 managers, err + } + defer client.Logout() + + // Obtain the ServiceRoot + rf_service := client.GetService() + log.Info().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") + } + return walkManagers(rf_managers, config.URI) } func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chassis, baseURI string) ([]InventoryDetail, error) { @@ -200,7 +247,44 @@ func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chass for _, rf_trustedmodule := range rf_computersystem.TrustedModules { system.TrustedModules = append(system.TrustedModules, fmt.Sprintf("%s %s", rf_trustedmodule.InterfaceType, rf_trustedmodule.FirmwareVersion)) } + systems = append(systems, system) } return systems, nil } + +func walkManagers(rf_managers []*redfish.Manager, baseURI string) ([]Manager, error) { + var managers []Manager + for _, rf_manager := range rf_managers { + rf_ethernetinterfaces, err := rf_manager.EthernetInterfaces() + if err != nil { + log.Error().Err(err).Msg("failed to get ethernet interfaces from manager") + return managers, err + } + var ethernet_interfaces []EthernetInterface + for _, rf_ethernetinterface := range rf_ethernetinterfaces { + if len(rf_ethernetinterface.IPv4Addresses) <= 0 { + continue + } + ethernet_interfaces = append(ethernet_interfaces, EthernetInterface{ + URI: baseURI + rf_ethernetinterface.ODataID, + MAC: rf_ethernetinterface.MACAddress, + Name: rf_ethernetinterface.Name, + Description: rf_ethernetinterface.Description, + Enabled: rf_ethernetinterface.InterfaceEnabled, + IP: rf_ethernetinterface.IPv4Addresses[0].Address, + }) + } + 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, + }) + } + return managers, nil +} diff --git a/tests/compatibility_test.go b/tests/compatibility_test.go index 03917ec..ce2e876 100644 --- a/tests/compatibility_test.go +++ b/tests/compatibility_test.go @@ -126,7 +126,7 @@ func TestExpectedOutput(t *testing.T) { t.Fatalf("failed while waiting for emulator: %v", err) } - systems, err := crawler.CrawlBMC( + systems, err := crawler.CrawlBMCForSystems( crawler.CrawlerConfig{ URI: *host, Username: *username,