Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add manager information from node BMC #62

Merged
merged 8 commits into from
Oct 25, 2024
2 changes: 1 addition & 1 deletion cmd/crawl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
114 changes: 105 additions & 9 deletions internal/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"os"
"path"
"strings"
"sync"
"time"

Expand All @@ -21,7 +22,9 @@ import (

"github.com/Cray-HPE/hms-xname/xnames"
_ "github.com/mattn/go-sqlite3"
"github.com/stmcginnis/gofish"
_ "github.com/stmcginnis/gofish"
synackd marked this conversation as resolved.
Show resolved Hide resolved
"github.com/stmcginnis/gofish/redfish"
"golang.org/x/exp/slices"
)

Expand All @@ -41,12 +44,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")
}
Expand Down Expand Up @@ -109,14 +113,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
Expand All @@ -129,9 +142,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)
Expand Down Expand Up @@ -225,3 +249,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")
}
94 changes: 87 additions & 7 deletions pkg/crawler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -200,7 +247,40 @@ 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 {
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,
})
}
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
}
2 changes: 1 addition & 1 deletion tests/compatibility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down