magellan/pkg/crawler/main.go

337 lines
13 KiB
Go

package crawler
import (
"encoding/json"
"fmt"
"strings"
"github.com/OpenCHAMI/magellan/pkg/secrets"
"github.com/rs/zerolog/log"
"github.com/stmcginnis/gofish"
"github.com/stmcginnis/gofish/redfish"
)
type CrawlerConfig struct {
URI string // URI of the BMC
Insecure bool // Whether to ignore SSL errors
CredentialStore secrets.SecretStore
}
func (cc *CrawlerConfig) GetUserPass() (BMCUsernamePassword, error) {
return loadBMCCreds(*cc)
}
type BMCUsernamePassword struct {
Username string `json:"username"`
Password string `json:"password"`
}
type EthernetInterface struct {
URI string `json:"uri,omitempty"` // URI of the interface
MAC string `json:"mac,omitempty"` // MAC address of the interface
IP string `json:"ip,omitempty"` // IP address of the interface
Name string `json:"name,omitempty"` // Name of the interface
Description string `json:"description,omitempty"` // Description of the interface
Enabled bool `json:"enabled,omitempty"` // Enabled interface
}
type NetworkAdapter struct {
URI string `json:"uri,omitempty"` // URI of the adapter
Manufacturer string `json:"manufacturer,omitempty"` // Manufacturer of the adapter
Name string `json:"name,omitempty"` // Name of the adapter
Model string `json:"model,omitempty"` // Model of the adapter
Serial string `json:"serial,omitempty"` // Serial number of the adapter
Description string `json:"description,omitempty"` // Description of the adapter
}
type NetworkInterface struct {
URI string `json:"uri,omitempty"` // URI of the interface
Name string `json:"name,omitempty"` // Name of the interface
Description string `json:"description,omitempty"` // Description of the interface
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
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
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
PowerState string `json:"power_state,omitempty"` // Power state of the Node
ProcessorCount int `json:"processor_count,omitempty"` // Processors of the Node
ProcessorType string `json:"processor_type,omitempty"` // Processor type of the Node
MemoryTotal float32 `json:"memory_total,omitempty"` // Total memory of the Node in Gigabytes
TrustedModules []string `json:"trusted_modules,omitempty"` // Trusted modules of the Node
TrustedComponents []string `json:"trusted_components,omitempty"` // Trusted components of the Chassis
Chassis_SKU string `json:"chassis_sku,omitempty"` // SKU of the Chassis
Chassis_Serial string `json:"chassis_serial,omitempty"` // Serial number of the Chassis
Chassis_AssetTag string `json:"chassis_asset_tag,omitempty"` // Asset tag of the Chassis
Chassis_Manufacturer string `json:"chassis_manufacturer,omitempty"` // Manufacturer of the Chassis
Chassis_Model string `json:"chassis_model,omitempty"` // Model of the Chassis
}
// 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
)
// get username and password from secret store
bmc_creds, err := loadBMCCreds(config)
if err != nil {
event := log.Error()
event.Err(err)
event.Msg("failed to load BMC credentials")
return nil, err
}
// initialize gofish client
client, err := gofish.Connect(gofish.ClientConfig{
Endpoint: config.URI,
Username: bmc_creds.Username,
Password: bmc_creds.Password,
Insecure: config.Insecure,
BasicAuth: true,
})
if err != nil {
if strings.HasPrefix(err.Error(), "404:") {
err = fmt.Errorf("no ServiceRoot found. This is probably not a BMC: %s", config.URI)
}
if strings.HasPrefix(err.Error(), "401:") {
err = fmt.Errorf("authentication failed. Check your username and password: %s", config.URI)
}
event := log.Error()
event.Err(err)
event.Msg("failed to connect to BMC")
return systems, 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)
// Nodes are sometimes only found under Chassis, but they should be found under Systems.
rf_chassis, err := rf_service.Chassis()
if err == nil {
log.Info().Msgf("found %d chassis in ServiceRoot", len(rf_chassis))
for _, chassis := range rf_chassis {
rf_chassis_systems, err := chassis.ComputerSystems()
if err == nil {
rf_systems = append(rf_systems, rf_chassis_systems...)
log.Info().Msgf("found %d systems in chassis %s", len(rf_chassis_systems), chassis.ID)
}
}
}
rf_root_systems, err := rf_service.Systems()
if err != nil {
log.Error().Err(err).Msg("failed to get systems from ServiceRoot")
}
log.Info().Msgf("found %d systems in ServiceRoot", len(rf_root_systems))
rf_systems = append(rf_systems, rf_root_systems...)
return walkSystems(rf_systems, nil, config.URI)
}
// CrawlBMCForSystems pulls BMC manager information.
func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) {
// get username and password from secret store
bmc_creds, err := loadBMCCreds(config)
if err != nil {
event := log.Error()
event.Err(err)
event.Msg("failed to load BMC credentials")
return nil, err
}
// initialize gofish client
var managers []Manager
client, err := gofish.Connect(gofish.ClientConfig{
Endpoint: config.URI,
Username: bmc_creds.Username,
Password: bmc_creds.Password,
Insecure: config.Insecure,
BasicAuth: true,
})
if err != nil {
if strings.HasPrefix(err.Error(), "404:") {
err = fmt.Errorf("no ServiceRoot found. This is probably not a BMC: %s", config.URI)
}
if strings.HasPrefix(err.Error(), "401:") {
err = fmt.Errorf("authentication failed. Check your username and password: %s", config.URI)
}
event := log.Error()
event.Err(err)
event.Msg("failed to connect to BMC")
return 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) {
systems := []InventoryDetail{}
for _, rf_computersystem := range rf_systems {
system := InventoryDetail{
URI: baseURI + "/redfish/v1/Systems/" + rf_computersystem.ID,
UUID: rf_computersystem.UUID,
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,
PowerState: string(rf_computersystem.PowerState),
ProcessorCount: rf_computersystem.ProcessorSummary.Count,
ProcessorType: rf_computersystem.ProcessorSummary.Model,
MemoryTotal: rf_computersystem.MemorySummary.TotalSystemMemoryGiB,
}
if rf_chassis != nil {
system.Chassis_SKU = rf_chassis.SKU
system.Chassis_Serial = rf_chassis.SerialNumber
system.Chassis_AssetTag = rf_chassis.AssetTag
system.Chassis_Manufacturer = rf_chassis.Manufacturer
system.Chassis_Model = rf_chassis.Model
}
rf_ethernetinterfaces, err := rf_computersystem.EthernetInterfaces()
if err != nil {
log.Error().Err(err).Msg("failed to get ethernet interfaces from computer system")
return systems, err
}
for _, rf_ethernetinterface := range rf_ethernetinterfaces {
ethernetinterface := EthernetInterface{
URI: baseURI + rf_ethernetinterface.ODataID,
MAC: rf_ethernetinterface.MACAddress,
Name: rf_ethernetinterface.Name,
Description: rf_ethernetinterface.Description,
Enabled: rf_ethernetinterface.InterfaceEnabled,
}
if len(rf_ethernetinterface.IPv4Addresses) > 0 {
ethernetinterface.IP = rf_ethernetinterface.IPv4Addresses[0].Address
}
system.EthernetInterfaces = append(system.EthernetInterfaces, ethernetinterface)
}
rf_networkInterfaces, err := rf_computersystem.NetworkInterfaces()
if err != nil {
log.Error().Err(err).Msg("failed to get network interfaces from computer system")
return systems, err
}
for _, rf_networkInterface := range rf_networkInterfaces {
rf_networkAdapter, err := rf_networkInterface.NetworkAdapter()
if err != nil {
log.Error().Err(err).Msg("failed to get network adapter from network interface")
return systems, err
}
var networkAdapter NetworkAdapter
if rf_networkAdapter != nil {
networkAdapter = NetworkAdapter{
URI: baseURI + rf_networkAdapter.ODataID,
Name: rf_networkAdapter.Name,
Manufacturer: rf_networkAdapter.Manufacturer,
Model: rf_networkAdapter.Model,
Serial: rf_networkAdapter.SerialNumber,
Description: rf_networkAdapter.Description,
}
}
networkInterface := NetworkInterface{
URI: baseURI + rf_networkInterface.ODataID,
Name: rf_networkInterface.Name,
Description: rf_networkInterface.Description,
Adapter: networkAdapter,
}
system.NetworkInterfaces = append(system.NetworkInterfaces, networkInterface)
}
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
}
func loadBMCCreds(config CrawlerConfig) (BMCUsernamePassword, error) {
creds, err := config.CredentialStore.GetSecretByID(config.URI)
if err != nil {
event := log.Error()
event.Err(err)
event.Msg("failed to get credentials from secret store")
return BMCUsernamePassword{}, err
}
var bmc_creds BMCUsernamePassword
err = json.Unmarshal([]byte(creds), &bmc_creds)
if err != nil {
event := log.Error()
event.Err(err)
event.Msg("failed to unmarshal credentials")
return BMCUsernamePassword{}, err
}
return bmc_creds, nil
}