Merge branch 'main' into update-docker

This commit is contained in:
David J. Allen 2023-10-19 17:11:07 -06:00
commit 90dff1f8e3
11 changed files with 494 additions and 144 deletions

View file

@ -1,7 +1,8 @@
# Magellan # Magellan
Magellan is a small tool designed to scan a network and collect BMC information Magellan is a board management controller discovery tool designed to scan a network
to load the data into an [`hms-smd`](https://github.com/alexlovelltroy/hms-smd/tree/master) instance. and collect information about a BMC node and load that data into an
[`hms-smd`](https://github.com/bikeshack/smd/tree/master) instance.
## How It Works ## How It Works
@ -13,9 +14,13 @@ Magellan is designed to do three things:
Magellan first tries to probe for specified hosts using the [`dora`](https://github.com/bmc-toolbox/dora) Magellan first tries to probe for specified hosts using the [`dora`](https://github.com/bmc-toolbox/dora)
API. If that fails, it then tries to use its own built-in, simpler scanner as a fallback. API. If that fails, it then tries to use its own built-in, simpler scanner as a fallback.
Next, it tries to query information about the BMC node using `bmclib` functions, but requires This is done by sending a raw TCP request to a number of potential hosts over a
access to a redfish interface on the node to work. Once the BMC information is received, network, and noting which requests are successful. At this point, `magellan` sees
it is then stored into `hms-smd` using its API. no difference between a services.
Next, it tries to query information about the BMC node using `bmclib` functions,
but requires access to a redfish interface on the node to work. Once the BMC
information is received, it is then stored into `hms-smd` using its API.
In summary, `magellan` needs at minimum the following configured to work on each node: In summary, `magellan` needs at minimum the following configured to work on each node:
@ -33,7 +38,7 @@ cd magellan
go mod tidy && go build go mod tidy && go build
``` ```
This should find and download all of the required dependencies. Although other This should find and download all of the required dependencies. Although other
versions of Go may work, the project has only been tested with v1.20. versions of Go may work, the project has only been tested with v1.20.
## Usage ## Usage

View file

@ -33,6 +33,7 @@ var collectCmd = &cobra.Command{
q := &magellan.QueryParams{ q := &magellan.QueryParams{
User: user, User: user,
Pass: pass, Pass: pass,
Protocol: protocol,
Drivers: drivers, Drivers: drivers,
Preferred: preferredDriver, Preferred: preferredDriver,
Timeout: timeout, Timeout: timeout,
@ -58,8 +59,9 @@ func init() {
collectCmd.PersistentFlags().IntVarP(&smd.Port, "port", "p", smd.Port, "set the port to the smd API") collectCmd.PersistentFlags().IntVarP(&smd.Port, "port", "p", smd.Port, "set the port to the smd API")
collectCmd.PersistentFlags().StringVar(&user, "user", "", "set the BMC user") collectCmd.PersistentFlags().StringVar(&user, "user", "", "set the BMC user")
collectCmd.PersistentFlags().StringVar(&pass, "pass", "", "set the BMC password") collectCmd.PersistentFlags().StringVar(&pass, "pass", "", "set the BMC password")
collectCmd.PersistentFlags().StringVar(&protocol, "protocol", "https", "set the Redfish protocol")
collectCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "/tmp/magellan/data/", "set the path to store collection data") collectCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "/tmp/magellan/data/", "set the path to store collection data")
collectCmd.PersistentFlags().BoolVar(&forceUpdate, "force-update", true, "set flag to force update data sent to SMD ") collectCmd.PersistentFlags().BoolVar(&forceUpdate, "force-update", false, "set flag to force update data sent to SMD ")
collectCmd.PersistentFlags().StringVar(&preferredDriver, "preferred-driver", "ipmi", "set the preferred driver to use") collectCmd.PersistentFlags().StringVar(&preferredDriver, "preferred-driver", "ipmi", "set the preferred driver to use")
collectCmd.PersistentFlags().StringVar(&ipmitoolPath, "ipmitool.path", "/usr/bin/ipmitool", "set the path for ipmitool") collectCmd.PersistentFlags().StringVar(&ipmitoolPath, "ipmitool.path", "/usr/bin/ipmitool", "set the path for ipmitool")
collectCmd.PersistentFlags().BoolVar(&withSecureTLS, "secure-tls", false, "enable secure TLS") collectCmd.PersistentFlags().BoolVar(&withSecureTLS, "secure-tls", false, "enable secure TLS")

View file

@ -12,6 +12,7 @@ var (
threads int threads int
ports []int ports []int
hosts []string hosts []string
protocol string
withSecureTLS bool withSecureTLS bool
certPoolFile string certPoolFile string
user string user string

View file

@ -16,6 +16,7 @@ var (
begin uint8 begin uint8
end uint8 end uint8
subnets []string subnets []string
disableProbing bool
) )
var scanCmd = &cobra.Command{ var scanCmd = &cobra.Command{
@ -44,7 +45,7 @@ var scanCmd = &cobra.Command{
if threads <= 0 { if threads <= 0 {
threads = mathutil.Clamp(len(hostsToScan), 1, 255) threads = mathutil.Clamp(len(hostsToScan), 1, 255)
} }
probeStates := magellan.ScanForAssets(hostsToScan, portsToScan, threads, timeout) probeStates := magellan.ScanForAssets(hostsToScan, portsToScan, threads, timeout, disableProbing)
for _, r := range probeStates { for _, r := range probeStates {
fmt.Printf("%s:%d (%s)\n", r.Host, r.Port, r.Protocol) fmt.Printf("%s:%d (%s)\n", r.Host, r.Port, r.Protocol)
} }
@ -60,11 +61,12 @@ var scanCmd = &cobra.Command{
} }
func init() { func init() {
scanCmd.PersistentFlags().StringSliceVar(&hosts, "host", []string{}, "set additional hosts to scan") scanCmd.Flags().StringSliceVar(&hosts, "host", []string{}, "set additional hosts to scan")
scanCmd.PersistentFlags().IntSliceVar(&ports, "port", []int{}, "set the ports to scan") scanCmd.Flags().IntSliceVar(&ports, "port", []int{}, "set the ports to scan")
scanCmd.Flags().Uint8Var(&begin, "begin", 0, "set the starting point for range of IP addresses") scanCmd.Flags().Uint8Var(&begin, "begin", 0, "set the starting point for range of IP addresses")
scanCmd.Flags().Uint8Var(&end, "end", 255, "set the ending point for range of IP addresses") scanCmd.Flags().Uint8Var(&end, "end", 255, "set the ending point for range of IP addresses")
scanCmd.Flags().StringSliceVar(&subnets, "subnet", []string{}, "set additional subnets") scanCmd.Flags().StringSliceVar(&subnets, "subnet", []string{}, "set additional subnets")
scanCmd.Flags().BoolVar(&disableProbing, "disable-probing", false, "disable probing scanned results for BMC nodes")
rootCmd.AddCommand(scanCmd) rootCmd.AddCommand(scanCmd)
} }

82
cmd/update.go Normal file
View file

@ -0,0 +1,82 @@
package cmd
import (
magellan "github.com/bikeshack/magellan/internal"
"github.com/bikeshack/magellan/internal/log"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
host string
port int
firmwarePath string
firmwareVersion string
component string
transferProtocol string
status bool
)
var updateCmd = &cobra.Command{
Use: "update",
Short: "Update BMC node firmware",
Run: func(cmd *cobra.Command, args []string) {
l := log.NewLogger(logrus.New(), logrus.DebugLevel)
q := &magellan.UpdateParams {
FirmwarePath: firmwarePath,
FirmwareVersion: firmwareVersion,
Component: component,
TransferProtocol: transferProtocol,
QueryParams: magellan.QueryParams{
Drivers: []string{"redfish"},
Preferred: "redfish",
Protocol: protocol,
Host: host,
User: user,
Pass: pass,
Timeout: timeout,
Port: port,
WithSecureTLS: withSecureTLS,
},
}
// check if required params are set
if host == "" || user == "" || pass == "" {
l.Log.Fatal("requires host, user, and pass to be set")
}
// get status if flag is set and exit
if status {
err := magellan.GetUpdateStatus(q)
if err != nil {
l.Log.Errorf("could not get update status: %v", err)
}
return
}
// client, err := magellan.NewClient(l, &q.QueryParams)
// if err != nil {
// l.Log.Errorf("could not make client: %v", err)
// }
// err = magellan.UpdateFirmware(client, l, q)
err := magellan.UpdateFirmwareRemote(q)
if err != nil {
l.Log.Errorf("could not update firmware: %v", err)
}
},
}
func init() {
updateCmd.Flags().StringVar(&host, "host", "", "set the BMC host")
updateCmd.Flags().IntVar(&port, "port", 443, "set the BMC port")
updateCmd.Flags().StringVar(&user, "user", "", "set the BMC user")
updateCmd.Flags().StringVar(&pass, "pass", "", "set the BMC password")
updateCmd.Flags().StringVar(&transferProtocol, "transfer-protocol", "HTTP", "set the transfer protocol")
updateCmd.Flags().StringVar(&protocol, "protocol", "https", "set the Redfish protocol")
updateCmd.Flags().StringVar(&firmwarePath, "firmware-path", "", "set the path to the firmware")
updateCmd.Flags().StringVar(&firmwareVersion, "firmware-version", "", "set the version of firmware to be installed")
updateCmd.Flags().StringVar(&component, "component", "", "set the component to upgrade")
updateCmd.Flags().BoolVar(&withSecureTLS, "secure-tls", false, "enable secure TLS")
updateCmd.Flags().BoolVar(&status, "status", false, "get the status of the update")
rootCmd.AddCommand(updateCmd)
}

View file

@ -51,16 +51,11 @@ func AddRedfishEndpoint(data []byte, headers map[string]string) error {
// Add redfish endpoint via POST `/hsm/v2/Inventory/RedfishEndpoints` endpoint // Add redfish endpoint via POST `/hsm/v2/Inventory/RedfishEndpoints` endpoint
url := makeEndpointUrl("/Inventory/RedfishEndpoints") url := makeEndpointUrl("/Inventory/RedfishEndpoints")
res, body, err := util.MakeRequest(url, "POST", data, headers) res, body, err := util.MakeRequest(url, "POST", data, headers)
if res == nil {
return fmt.Errorf("no response")
}
fmt.Printf("smd url: %v\n", url)
fmt.Printf("res: %v\n", res.Status)
fmt.Printf("body: %v\n", string(body))
if res != nil { if res != nil {
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
return fmt.Errorf("could not add redfish endpoint") return fmt.Errorf("could not add redfish endpoint")
} }
fmt.Printf("%v (%v)\n%s\n", url, res.Status, string(body))
} }
return err return err
} }
@ -72,12 +67,7 @@ func UpdateRedfishEndpoint(xname string, data []byte, headers map[string]string)
// Update redfish endpoint via PUT `/hsm/v2/Inventory/RedfishEndpoints` endpoint // Update redfish endpoint via PUT `/hsm/v2/Inventory/RedfishEndpoints` endpoint
url := makeEndpointUrl("/Inventory/RedfishEndpoints/" + xname) url := makeEndpointUrl("/Inventory/RedfishEndpoints/" + xname)
res, body, err := util.MakeRequest(url, "PUT", data, headers) res, body, err := util.MakeRequest(url, "PUT", data, headers)
if res == nil { fmt.Printf("%v (%v)\n%s\n", url, res.Status, string(body))
return fmt.Errorf("no response")
}
fmt.Printf("smd url: %v\n", url)
fmt.Printf("res: %v\n", res.Status)
fmt.Printf("body: %v\n", string(body))
if res != nil { if res != nil {
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
return fmt.Errorf("could not update redfish endpoint") return fmt.Errorf("could not update redfish endpoint")

View file

@ -33,17 +33,12 @@ const (
HTTPS_PORT = 443 HTTPS_PORT = 443
) )
type BMCProbeResult struct {
Host string `json:"host"`
Port int `json:"port"`
Protocol string `json:"protocol"`
State bool `json:"state"`
}
// NOTE: ...params were getting too long... // NOTE: ...params were getting too long...
type QueryParams struct { type QueryParams struct {
Host string Host string
Port int Port int
Protocol string
User string User string
Pass string Pass string
Drivers []string Drivers []string
@ -59,9 +54,6 @@ type QueryParams struct {
} }
func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) { func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) {
// NOTE: bmclib.NewClient(host, port, user, pass)
// ...seems like the `port` params doesn't work like expected depending on interface
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
} }
@ -83,22 +75,20 @@ func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) {
} }
// only work if valid cert is provided // only work if valid cert is provided
if q.WithSecureTLS { if q.WithSecureTLS && q.CertPoolFile != "" {
var pool *x509.CertPool pool := x509.NewCertPool()
if q.CertPoolFile != "" { data, err := os.ReadFile(q.CertPoolFile)
pool = x509.NewCertPool() if err != nil {
data, err := os.ReadFile(q.CertPoolFile) return nil, fmt.Errorf("could not read cert pool file: %v", err)
if err != nil {
return nil, fmt.Errorf("could not read cert pool file: %v", err)
}
pool.AppendCertsFromPEM(data)
} }
pool.AppendCertsFromPEM(data)
// a nil pool uses the system certs // a nil pool uses the system certs
clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool))
} }
url := "" url := ""
fmt.Println(url)
if q.User != "" && q.Pass != "" { if q.User != "" && q.Pass != "" {
url += fmt.Sprintf("https://%s:%s@%s", q.User, q.Pass, q.Host) url += fmt.Sprintf("%s://%s:%s@%s", q.Protocol, q.User, q.Pass, q.Host)
} else { } else {
url += q.Host url += q.Host
} }
@ -112,7 +102,7 @@ func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) {
return client, nil return client, nil
} }
func CollectInfo(probeStates *[]BMCProbeResult, l *log.Logger, q *QueryParams) error { func CollectInfo(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) error {
// check for available probe states // check for available probe states
if probeStates == nil { if probeStates == nil {
return fmt.Errorf("no probe states found") return fmt.Errorf("no probe states found")
@ -130,7 +120,7 @@ func CollectInfo(probeStates *[]BMCProbeResult, l *log.Logger, q *QueryParams) e
found := make([]string, 0, len(*probeStates)) found := make([]string, 0, len(*probeStates))
done := make(chan struct{}, q.Threads+1) done := make(chan struct{}, q.Threads+1)
chanProbeState := make(chan BMCProbeResult, q.Threads+1) chanProbeState := make(chan ScannedResult, q.Threads+1)
// generate custom xnames for bmcs // generate custom xnames for bmcs
node := xnames.Node{ node := xnames.Node{
@ -163,22 +153,22 @@ func CollectInfo(probeStates *[]BMCProbeResult, l *log.Logger, q *QueryParams) e
node.NodeBMC += 1 node.NodeBMC += 1
// data to be sent to smd // data to be sent to smd
data := make(map[string]any) data := map[string]any{
data["ID"] = fmt.Sprintf("%v", node.String()[:len(node.String())-2]) "ID": fmt.Sprintf("%v", node.String()[:len(node.String())-2]),
data["Type"] = "" "Type": "",
data["Name"] = "" "Name": "",
data["FQDN"] = ps.Host "FQDN": ps.Host,
data["User"] = q.User "User": q.User,
data["Password"] = q.Pass "Password": q.Pass,
data["IPAddr"] = "" "MACRequired": true,
data["MACAddr"] = "" "RediscoverOnUpdate": false,
data["RediscoverOnUpdate"] = false }
// unmarshal json to send in correct format // unmarshal json to send in correct format
var rm map[string]json.RawMessage var rm map[string]json.RawMessage
// inventories // inventories
inventory, err := QueryInventory(client, l, q) inventory, err := QueryInventory(client, q)
if err != nil { if err != nil {
l.Log.Errorf("could not query inventory (%v:%v): %v", q.Host, q.Port, err) l.Log.Errorf("could not query inventory (%v:%v): %v", q.Host, q.Port, err)
} }
@ -195,24 +185,13 @@ func CollectInfo(probeStates *[]BMCProbeResult, l *log.Logger, q *QueryParams) e
data["Chassis"] = rm["Chassis"] data["Chassis"] = rm["Chassis"]
// ethernet interfaces // ethernet interfaces
interfaces, err := QueryEthernetInterfaces(client, l, q) // interfaces, err := QueryEthernetInterfaces(client, q)
if err != nil { // if err != nil {
l.Log.Errorf("could not query ethernet interfaces: %v", err) // l.Log.Errorf("could not query ethernet interfaces: %v", err)
continue // continue
} // }
json.Unmarshal(interfaces, &rm) // json.Unmarshal(interfaces, &rm)
data["Interface"] = rm["Interface"] // data["Interfaces"] = rm["Interfaces"]
// get MAC address of first interface (for now...)
if len(rm["Interface"]) > 0 {
var i map[string]interface{}
json.Unmarshal(rm["Interface"], &i)
data["MACAddr"] = i["MACAddress"]
data["IPAddr"] = i["IPAddress"]
if i["FQDN"] != "" {
data["FQDN"] = rm["FQDN"]
}
}
// storage // storage
// storage, err := QueryStorage(q) // storage, err := QueryStorage(q)
@ -224,16 +203,16 @@ func CollectInfo(probeStates *[]BMCProbeResult, l *log.Logger, q *QueryParams) e
// data["Storage"] = rm["Storage"] // data["Storage"] = rm["Storage"]
// get specific processor info // get specific processor info
procs, err := QueryProcessors(q) // procs, err := QueryProcessors(q)
if err != nil { // if err != nil {
l.Log.Errorf("could not query processors: %v", err) // l.Log.Errorf("could not query processors: %v", err)
} // }
var p map[string]interface{} // var p map[string]interface{}
json.Unmarshal(procs, &p) // json.Unmarshal(procs, &p)
data["Processors"] = rm["Processors"] // data["Processors"] = rm["Processors"]
// systems // systems
systems, err := QuerySystems(q) systems, err := QuerySystems(client, q)
if err != nil { if err != nil {
l.Log.Errorf("could not query systems: %v", err) l.Log.Errorf("could not query systems: %v", err)
} }
@ -318,7 +297,7 @@ func CollectInfo(probeStates *[]BMCProbeResult, l *log.Logger, q *QueryParams) e
return nil return nil
} }
func QueryMetadata(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryMetadata(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// client, err := NewClient(l, q) // client, err := NewClient(l, q)
// open BMC session and update driver registry // open BMC session and update driver registry
@ -352,7 +331,7 @@ func QueryMetadata(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte
return b, nil return b, nil
} }
func QueryInventory(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryInventory(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// open BMC session and update driver registry // open BMC session and update driver registry
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout)) ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx) client.Registry.FilterForCompatible(ctx)
@ -385,7 +364,7 @@ func QueryInventory(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byt
return b, nil return b, nil
} }
func QueryPowerState(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryPowerState(client *bmclib.Client, q *QueryParams) ([]byte, error) {
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout)) ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx) client.Registry.FilterForCompatible(ctx)
err := client.PreferProvider(q.Preferred).Open(ctx) err := client.PreferProvider(q.Preferred).Open(ctx)
@ -417,7 +396,7 @@ func QueryPowerState(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]by
} }
func QueryUsers(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryUsers(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// open BMC session and update driver registry // open BMC session and update driver registry
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout)) ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx) client.Registry.FilterForCompatible(ctx)
@ -451,7 +430,7 @@ func QueryUsers(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, e
return b, nil return b, nil
} }
func QueryBios(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryBios(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// client, err := NewClient(l, q) // client, err := NewClient(l, q)
// if err != nil { // if err != nil {
// return nil, fmt.Errorf("could not make query: %v", err) // return nil, fmt.Errorf("could not make query: %v", err)
@ -463,26 +442,39 @@ func QueryBios(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, er
return b, err return b, err
} }
func QueryEthernetInterfaces(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, error) { func QueryEthernetInterfaces(client *bmclib.Client, q *QueryParams, systemID string) ([]byte, error) {
c, err := connectGofish(q) c, err := connectGofish(q)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not connect to bmc: %v", err) return nil, fmt.Errorf("could not connect to bmc: %v", err)
} }
interfaces, err := redfish.ListReferencedEthernetInterfaces(c, "/redfish/v1/Systems/") systems, err := c.Service.Systems()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not query storage systems (%v:%v): %v", q.Host, q.Port, err)
}
var interfaces []*redfish.EthernetInterface
for _, system := range systems {
i, err := redfish.ListReferencedEthernetInterfaces(c, "/redfish/v1/Systems/" + system.ID + "/EthernetInterfaces/")
if err != nil {
continue
}
interfaces = append(interfaces, i...)
}
if len(interfaces) <= 0 {
return nil, fmt.Errorf("could not get ethernet interfaces: %v", err) return nil, fmt.Errorf("could not get ethernet interfaces: %v", err)
} }
data := map[string]any{"Interfaces": interfaces} data := map[string]any{"EthernetInterfaces": interfaces}
b, err := json.MarshalIndent(data, "", " ") b, err := json.MarshalIndent(data, "", " ")
if err != nil { if err != nil {
return nil, fmt.Errorf("could not marshal JSON: %v", err) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
if q.Verbose { // if q.Verbose {
fmt.Printf("%v\n", string(b)) // fmt.Printf("%v\n", string(b))
} // }
return b, nil return b, nil
} }
@ -541,7 +533,7 @@ func QueryStorage(q *QueryParams) ([]byte, error) {
return b, nil return b, nil
} }
func QuerySystems(q *QueryParams) ([]byte, error) { func QuerySystems(client *bmclib.Client, q *QueryParams) ([]byte, error) {
c, err := connectGofish(q) c, err := connectGofish(q)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not connect to bmc (%v:%v): %v", q.Host, q.Port, err) return nil, fmt.Errorf("could not connect to bmc (%v:%v): %v", q.Host, q.Port, err)
@ -549,10 +541,25 @@ func QuerySystems(q *QueryParams) ([]byte, error) {
systems, err := c.Service.Systems() systems, err := c.Service.Systems()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not query storage systems (%v:%v): %v", q.Host, q.Port, err) return nil, fmt.Errorf("could not query systems (%v:%v): %v", q.Host, q.Port, err)
} }
data := map[string]any{"Systems": systems } // query the system's ethernet interfaces
var temp []map[string]any
for _, system := range systems {
interfaces, err := QueryEthernetInterfaces(client, q, system.ID)
if err != nil {
continue
}
var i map[string]any
json.Unmarshal(interfaces, &i)
temp = append(temp, map[string]any{
"Data": system,
"EthernetInterfaces": i["EthernetInterfaces"],
})
}
data := map[string]any{"Systems": temp }
b, err := json.MarshalIndent(data, "", " ") b, err := json.MarshalIndent(data, "", " ")
if err != nil { if err != nil {
return nil, fmt.Errorf("could not marshal JSON: %v", err) return nil, fmt.Errorf("could not marshal JSON: %v", err)
@ -588,12 +595,7 @@ func QueryRegisteries(q *QueryParams) ([]byte, error) {
} }
func QueryProcessors(q *QueryParams) ([]byte, error) { func QueryProcessors(q *QueryParams) ([]byte, error) {
baseUrl := "https://" url := baseRedfishUrl(q) + "/Systems"
if q.User != "" && q.Pass != "" {
baseUrl += fmt.Sprintf("%s:%s@", q.User, q.Pass)
}
baseUrl += fmt.Sprintf("%s:%d", q.Host, q.Port)
url := baseUrl + "/redfish/v1/Systems"
res, body, err := util.MakeRequest(url, "GET", nil, nil) res, body, err := util.MakeRequest(url, "GET", nil, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("something went wrong: %v", err) return nil, fmt.Errorf("something went wrong: %v", err)
@ -612,7 +614,7 @@ func QueryProcessors(q *QueryParams) ([]byte, error) {
// request data about each processor member on node // request data about each processor member on node
for _, member := range members { for _, member := range members {
var oid = member["@odata.id"].(string) var oid = member["@odata.id"].(string)
var infoUrl = baseUrl + oid var infoUrl = url + oid
res, _, err := util.MakeRequest(infoUrl, "GET", nil, nil) res, _, err := util.MakeRequest(infoUrl, "GET", nil, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("something went wrong: %v", err) return nil, fmt.Errorf("something went wrong: %v", err)
@ -638,22 +640,23 @@ func QueryProcessors(q *QueryParams) ([]byte, error) {
func connectGofish(q *QueryParams) (*gofish.APIClient, error) { func connectGofish(q *QueryParams) (*gofish.APIClient, error) {
config := makeGofishConfig(q) config := makeGofishConfig(q)
c, err := gofish.Connect(config) c, err := gofish.Connect(config)
c.Service.ProtocolFeaturesSupported = gofish.ProtocolFeaturesSupported{ if err != nil {
ExpandQuery: gofish.Expand{
ExpandAll: true, return nil, fmt.Errorf("could not connect to redfish endpoint: %v", err)
Links: true, }
}, if c != nil {
c.Service.ProtocolFeaturesSupported = gofish.ProtocolFeaturesSupported{
ExpandQuery: gofish.Expand{
ExpandAll: true,
Links: true,
},
}
} }
return c, err return c, err
} }
func makeGofishConfig(q *QueryParams) gofish.ClientConfig { func makeGofishConfig(q *QueryParams) gofish.ClientConfig {
url := "https://" url := baseRedfishUrl(q)
if q.User != "" && q.Pass != "" {
url += fmt.Sprintf("%s:%s@", q.User, q.Pass)
}
url += fmt.Sprintf("%s:%d", q.Host, q.Port)
return gofish.ClientConfig{ return gofish.ClientConfig{
Endpoint: url, Endpoint: url,
Username: q.User, Username: q.User,
@ -691,3 +694,11 @@ func makeJson(object any) ([]byte, error) {
} }
return []byte(b), nil return []byte(b), nil
} }
func baseRedfishUrl(q *QueryParams) string {
url := fmt.Sprintf("%s://", q.Protocol)
if q.User != "" && q.Pass != "" {
url += fmt.Sprintf("%s:%s@", q.User, q.Pass)
}
return fmt.Sprintf("%s%s:%d", url, q.Host, q.Port)
}

View file

@ -8,12 +8,7 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
func InsertProbeResults(path string, states *[]magellan.BMCProbeResult) error { func CreateProbeResultsIfNotExists(path string) (*sqlx.DB, error) {
if states == nil {
return fmt.Errorf("states == nil")
}
// create database if it doesn't already exist
schema := ` schema := `
CREATE TABLE IF NOT EXISTS magellan_scanned_ports ( CREATE TABLE IF NOT EXISTS magellan_scanned_ports (
host TEXT NOT NULL, host TEXT NOT NULL,
@ -25,9 +20,22 @@ func InsertProbeResults(path string, states *[]magellan.BMCProbeResult) error {
` `
db, err := sqlx.Open("sqlite3", path) db, err := sqlx.Open("sqlite3", path)
if err != nil { if err != nil {
return fmt.Errorf("could not open database: %v", err) return nil, fmt.Errorf("could not open database: %v", err)
} }
db.MustExec(schema) db.MustExec(schema)
return db, nil
}
func InsertProbeResults(path string, states *[]magellan.ScannedResult) error {
if states == nil {
return fmt.Errorf("states == nil")
}
// create database if it doesn't already exist
db, err := CreateProbeResultsIfNotExists(path)
if err != nil {
return err
}
// insert all probe states into db // insert all probe states into db
tx := db.MustBegin() tx := db.MustBegin()
@ -46,13 +54,37 @@ func InsertProbeResults(path string, states *[]magellan.BMCProbeResult) error {
return nil return nil
} }
func GetProbeResults(path string) ([]magellan.BMCProbeResult, error) { func DeleteProbeResults(path string, results *[]magellan.ScannedResult) error {
if results == nil {
return fmt.Errorf("no probe results found")
}
db, err := sqlx.Open("sqlite3", path)
if err != nil {
return fmt.Errorf("could not open database: %v", err)
}
tx := db.MustBegin()
for _, state := range *results {
sql := `DELETE FROM magellan_scanned_ports WHERE host = :host, port = :port;`
_, err := tx.NamedExec(sql, &state)
if err != nil {
fmt.Printf("could not execute transaction: %v\n", err)
}
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("could not commit transaction: %v", err)
}
return nil
}
func GetProbeResults(path string) ([]magellan.ScannedResult, error) {
db, err := sqlx.Open("sqlite3", path) db, err := sqlx.Open("sqlite3", path)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not open database: %v", err) return nil, fmt.Errorf("could not open database: %v", err)
} }
results := []magellan.BMCProbeResult{} results := []magellan.ScannedResult{}
err = db.Select(&results, "SELECT * FROM magellan_scanned_ports ORDER BY host ASC, port ASC;") err = db.Select(&results, "SELECT * FROM magellan_scanned_ports ORDER BY host ASC, port ASC;")
if err != nil { if err != nil {
return nil, fmt.Errorf("could not retrieve probes: %v", err) return nil, fmt.Errorf("could not retrieve probes: %v", err)

View file

@ -3,14 +3,24 @@ package magellan
import ( import (
"fmt" "fmt"
"net" "net"
"net/http"
"sync" "sync"
"time" "time"
"github.com/bikeshack/magellan/internal/util"
) )
func rawConnect(host string, ports []int, timeout int, keepOpenOnly bool) []BMCProbeResult { type ScannedResult struct {
results := []BMCProbeResult{} Host string `json:"host"`
Port int `json:"port"`
Protocol string `json:"protocol"`
State bool `json:"state"`
}
func rawConnect(host string, ports []int, timeout int, keepOpenOnly bool) []ScannedResult {
results := []ScannedResult{}
for _, p := range ports { for _, p := range ports {
result := BMCProbeResult{ result := ScannedResult{
Host: host, Host: host,
Port: p, Port: p,
Protocol: "tcp", Protocol: "tcp",
@ -50,8 +60,8 @@ func GenerateHosts(subnet string, begin uint8, end uint8) []string {
return hosts return hosts
} }
func ScanForAssets(hosts []string, ports []int, threads int, timeout int) []BMCProbeResult { func ScanForAssets(hosts []string, ports []int, threads int, timeout int, disableProbing bool) []ScannedResult {
results := make([]BMCProbeResult, 0, len(hosts)) results := make([]ScannedResult, 0, len(hosts))
done := make(chan struct{}, threads+1) done := make(chan struct{}, threads+1)
chanHost := make(chan string, threads+1) chanHost := make(chan string, threads+1)
// chanPort := make(chan int, threads+1) // chanPort := make(chan int, threads+1)
@ -66,8 +76,25 @@ func ScanForAssets(hosts []string, ports []int, threads int, timeout int) []BMCP
wg.Done() wg.Done()
return return
} }
s := rawConnect(host, ports, timeout, true) scannedResults := rawConnect(host, ports, timeout, true)
results = append(results, s...) if !disableProbing {
probeResults := []ScannedResult{}
for _, result := range scannedResults {
url := fmt.Sprintf("https://%s:%d/redfish/v1/", result.Host, result.Port)
res, _, err := util.MakeRequest(url, "GET", nil, nil)
if err != nil || res == nil {
continue
} else if res.StatusCode != http.StatusOK {
continue
} else {
probeResults = append(probeResults, result)
}
}
results = append(results, probeResults...)
} else {
results = append(results, scannedResults...)
}
} }
}() }()
} }

195
internal/update.go Normal file
View file

@ -0,0 +1,195 @@
package magellan
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/bikeshack/magellan/internal/log"
"github.com/bikeshack/magellan/internal/util"
bmclib "github.com/bmc-toolbox/bmclib/v2"
"github.com/bmc-toolbox/bmclib/v2/constants"
bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors"
"github.com/sirupsen/logrus"
)
type UpdateParams struct {
QueryParams
FirmwarePath string
FirmwareVersion string
Component string
TransferProtocol string
}
// NOTE: Does not work since OpenBMC, whic bmclib uses underneath, does not
// support multipart updates. See issue: https://github.com/bmc-toolbox/bmclib/issues/341
func UpdateFirmware(client *bmclib.Client, l *log.Logger, q *UpdateParams) error {
if q.Component == "" {
return fmt.Errorf("component is required")
}
// open BMC session and update driver registry
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx)
err := client.Open(ctx)
if err != nil {
ctxCancel()
return fmt.Errorf("could not connect to bmc: %v", err)
}
defer client.Close(ctx)
file, err := os.Open(q.FirmwarePath)
if err != nil {
ctxCancel()
return fmt.Errorf("could not open firmware path: %v", err)
}
defer file.Close()
taskId, err := client.FirmwareInstall(ctx, q.Component, constants.FirmwareApplyOnReset, true, file)
if err != nil {
ctxCancel()
return fmt.Errorf("could not install firmware: %v", err)
}
for {
if ctx.Err() != nil {
ctxCancel()
return fmt.Errorf("context error: %v", ctx.Err())
}
state, err := client.FirmwareInstallStatus(ctx, q.FirmwareVersion, q.Component, taskId)
if err != nil {
// when its under update a connection refused is returned
if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "operation timed out") {
l.Log.Info("BMC refused connection, BMC most likely resetting...")
time.Sleep(2 * time.Second)
continue
}
if errors.Is(err, bmclibErrs.ErrSessionExpired) || strings.Contains(err.Error(), "session expired") {
err := client.Open(ctx)
if err != nil {
l.Log.Fatal(err, "bmc re-login failed")
}
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("BMC session expired, logging in...")
continue
}
l.Log.Fatal(err)
}
switch state {
case constants.FirmwareInstallRunning, constants.FirmwareInstallInitializing:
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("firmware install running")
case constants.FirmwareInstallFailed:
ctxCancel()
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("firmware install failed")
return fmt.Errorf("failed to install firmware")
case constants.FirmwareInstallComplete:
ctxCancel()
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("firmware install completed")
return nil
case constants.FirmwareInstallPowerCyleHost:
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("host powercycle required")
if _, err := client.SetPowerState(ctx, "cycle"); err != nil {
ctxCancel()
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("error power cycling host for install")
return fmt.Errorf("failed to install firmware")
}
ctxCancel()
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("host power cycled, all done!")
return nil
default:
l.Log.WithFields(logrus.Fields{"state": state, "component": q.Component}).Info("unknown state returned")
}
time.Sleep(2 * time.Second)
}
return nil
}
func UpdateFirmwareRemote(q *UpdateParams) error {
url := baseRedfishUrl(&q.QueryParams) + "/redfish/v1/UpdateService/Actions/SimpleUpdate"
headers := map[string]string {
"Content-Type": "application/json",
"cache-control": "no-cache",
}
b := map[string]any{
"UpdateComponent": q.Component, // BMC, BIOS
"TransferProtocol": q.TransferProtocol,
"ImageURI": q.FirmwarePath,
}
data, err := json.Marshal(b)
if err != nil {
return fmt.Errorf("could not marshal data: %v", err)
}
res, body, err := util.MakeRequest(url, "POST", data, headers)
if err != nil {
return fmt.Errorf("something went wrong: %v", err)
} else if res == nil {
return fmt.Errorf("no response returned (url: %s)", url)
}
if len(body) > 0 {
fmt.Printf("%d: %v\n", res.StatusCode, string(body))
}
return nil
}
func GetUpdateStatus(q *UpdateParams) error {
url := baseRedfishUrl(&q.QueryParams) + "/redfish/v1/UpdateService"
res, body, err := util.MakeRequest(url, "GET", nil, nil)
if err != nil {
return fmt.Errorf("something went wrong: %v", err)
} else if res == nil {
return fmt.Errorf("no response returned (url: %s)", url)
} else if res.StatusCode != http.StatusOK {
return fmt.Errorf("returned status code %d", res.StatusCode)
}
if len(body) > 0 {
fmt.Printf("%d: %v\n", res.StatusCode, string(body))
}
return nil
}
// func UpdateFirmwareLocal(q *UpdateParams) error {
// fwUrl := baseUrl(&q.QueryParams) + ""
// url := baseUrl(&q.QueryParams) + "UpdateService/Actions/"
// headers := map[string]string {
// }
// // get etag from FW inventory
// response, err := util.MakeRequest()
// // load file from disk
// file, err := os.ReadFile(q.FirmwarePath)
// if err != nil {
// return fmt.Errorf("could not read file: %v", err)
// }
// switch q.TransferProtocol {
// case "HTTP":
// default:
// return fmt.Errorf("transfer protocol not supported")
// }
// return nil
// }

View file

@ -11,15 +11,18 @@ import (
) )
func PathExists(path string) (bool, error) { func PathExists(path string) (bool, error) {
_, err := os.Stat(path) _, err := os.Stat(path)
if err == nil { return true, nil } if err == nil { return true, nil }
if os.IsNotExist(err) { return false, nil } if os.IsNotExist(err) { return false, nil }
return false, err return false, err
} }
func MakeRequest(url string, httpMethod string, body []byte, headers map[string]string) (*http.Response, []byte, error) { func MakeRequest(url string, httpMethod string, body []byte, headers map[string]string) (*http.Response, []byte, error) {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
req, _ := http.NewRequest(httpMethod, url, bytes.NewBuffer(body)) req, err := http.NewRequest(httpMethod, url, bytes.NewBuffer(body))
if err != nil {
return nil, nil, fmt.Errorf("could not create new HTTP request: %v", err)
}
req.Header.Add("User-Agent", "magellan") req.Header.Add("User-Agent", "magellan")
for k, v := range headers { for k, v := range headers {
req.Header.Add(k, v) req.Header.Add(k, v)
@ -37,20 +40,20 @@ func MakeRequest(url string, httpMethod string, body []byte, headers map[string]
} }
func MakeOutputDirectory(path string) (string, error) { func MakeOutputDirectory(path string) (string, error) {
// get the current data + time using Go's stupid formatting // get the current data + time using Go's stupid formatting
t := time.Now() t := time.Now()
dirname := t.Format("2006-01-01 15:04:05") dirname := t.Format("2006-01-01 15:04:05")
final := path + "/" + dirname final := path + "/" + dirname
// check if path is valid and directory // check if path is valid and directory
pathExists, err := PathExists(final); pathExists, err := PathExists(final);
if err != nil { if err != nil {
return final, fmt.Errorf("could not check for existing path: %v", err) return final, fmt.Errorf("could not check for existing path: %v", err)
} }
if pathExists { if pathExists {
// make sure it is directory with 0o644 permissions // make sure it is directory with 0o644 permissions
return final, fmt.Errorf("found existing path: %v", final) return final, fmt.Errorf("found existing path: %v", final)
} }
// create directory with data + time // create directory with data + time
err = os.MkdirAll(final, 0766) err = os.MkdirAll(final, 0766)