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 is a small tool designed to scan a network and collect BMC information
to load the data into an [`hms-smd`](https://github.com/alexlovelltroy/hms-smd/tree/master) instance.
Magellan is a board management controller discovery tool designed to scan a network
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
@ -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)
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
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.
This is done by sending a raw TCP request to a number of potential hosts over a
network, and noting which requests are successful. At this point, `magellan` sees
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:
@ -33,7 +38,7 @@ cd magellan
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.
## Usage

View file

@ -33,6 +33,7 @@ var collectCmd = &cobra.Command{
q := &magellan.QueryParams{
User: user,
Pass: pass,
Protocol: protocol,
Drivers: drivers,
Preferred: preferredDriver,
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().StringVar(&user, "user", "", "set the BMC user")
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().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(&ipmitoolPath, "ipmitool.path", "/usr/bin/ipmitool", "set the path for ipmitool")
collectCmd.PersistentFlags().BoolVar(&withSecureTLS, "secure-tls", false, "enable secure TLS")

View file

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

View file

@ -16,6 +16,7 @@ var (
begin uint8
end uint8
subnets []string
disableProbing bool
)
var scanCmd = &cobra.Command{
@ -44,7 +45,7 @@ var scanCmd = &cobra.Command{
if threads <= 0 {
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 {
fmt.Printf("%s:%d (%s)\n", r.Host, r.Port, r.Protocol)
}
@ -60,11 +61,12 @@ var scanCmd = &cobra.Command{
}
func init() {
scanCmd.PersistentFlags().StringSliceVar(&hosts, "host", []string{}, "set additional hosts to scan")
scanCmd.PersistentFlags().IntSliceVar(&ports, "port", []int{}, "set the ports to scan")
scanCmd.Flags().StringSliceVar(&hosts, "host", []string{}, "set additional hosts 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(&end, "end", 255, "set the ending point for range of IP addresses")
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)
}

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
url := makeEndpointUrl("/Inventory/RedfishEndpoints")
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.StatusCode != http.StatusOK {
return fmt.Errorf("could not add redfish endpoint")
}
fmt.Printf("%v (%v)\n%s\n", url, res.Status, string(body))
}
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
url := makeEndpointUrl("/Inventory/RedfishEndpoints/" + xname)
res, body, err := util.MakeRequest(url, "PUT", 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))
fmt.Printf("%v (%v)\n%s\n", url, res.Status, string(body))
if res != nil {
if res.StatusCode != http.StatusOK {
return fmt.Errorf("could not update redfish endpoint")

View file

@ -33,17 +33,12 @@ const (
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...
type QueryParams struct {
Host string
Port int
Protocol string
User string
Pass string
Drivers []string
@ -59,9 +54,6 @@ type QueryParams struct {
}
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{
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
if q.WithSecureTLS {
var pool *x509.CertPool
if q.CertPoolFile != "" {
pool = x509.NewCertPool()
data, err := os.ReadFile(q.CertPoolFile)
if err != nil {
return nil, fmt.Errorf("could not read cert pool file: %v", err)
}
pool.AppendCertsFromPEM(data)
if q.WithSecureTLS && q.CertPoolFile != "" {
pool := x509.NewCertPool()
data, err := os.ReadFile(q.CertPoolFile)
if err != nil {
return nil, fmt.Errorf("could not read cert pool file: %v", err)
}
pool.AppendCertsFromPEM(data)
// a nil pool uses the system certs
clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool))
}
url := ""
fmt.Println(url)
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 {
url += q.Host
}
@ -112,7 +102,7 @@ func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) {
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
if probeStates == nil {
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))
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
node := xnames.Node{
@ -163,22 +153,22 @@ func CollectInfo(probeStates *[]BMCProbeResult, l *log.Logger, q *QueryParams) e
node.NodeBMC += 1
// data to be sent to smd
data := make(map[string]any)
data["ID"] = fmt.Sprintf("%v", node.String()[:len(node.String())-2])
data["Type"] = ""
data["Name"] = ""
data["FQDN"] = ps.Host
data["User"] = q.User
data["Password"] = q.Pass
data["IPAddr"] = ""
data["MACAddr"] = ""
data["RediscoverOnUpdate"] = false
data := map[string]any{
"ID": fmt.Sprintf("%v", node.String()[:len(node.String())-2]),
"Type": "",
"Name": "",
"FQDN": ps.Host,
"User": q.User,
"Password": q.Pass,
"MACRequired": true,
"RediscoverOnUpdate": false,
}
// unmarshal json to send in correct format
var rm map[string]json.RawMessage
// inventories
inventory, err := QueryInventory(client, l, q)
inventory, err := QueryInventory(client, q)
if err != nil {
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"]
// ethernet interfaces
interfaces, err := QueryEthernetInterfaces(client, l, q)
if err != nil {
l.Log.Errorf("could not query ethernet interfaces: %v", err)
continue
}
json.Unmarshal(interfaces, &rm)
data["Interface"] = rm["Interface"]
// 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"]
}
}
// interfaces, err := QueryEthernetInterfaces(client, q)
// if err != nil {
// l.Log.Errorf("could not query ethernet interfaces: %v", err)
// continue
// }
// json.Unmarshal(interfaces, &rm)
// data["Interfaces"] = rm["Interfaces"]
// storage
// storage, err := QueryStorage(q)
@ -224,16 +203,16 @@ func CollectInfo(probeStates *[]BMCProbeResult, l *log.Logger, q *QueryParams) e
// data["Storage"] = rm["Storage"]
// get specific processor info
procs, err := QueryProcessors(q)
if err != nil {
l.Log.Errorf("could not query processors: %v", err)
}
var p map[string]interface{}
json.Unmarshal(procs, &p)
data["Processors"] = rm["Processors"]
// procs, err := QueryProcessors(q)
// if err != nil {
// l.Log.Errorf("could not query processors: %v", err)
// }
// var p map[string]interface{}
// json.Unmarshal(procs, &p)
// data["Processors"] = rm["Processors"]
// systems
systems, err := QuerySystems(q)
systems, err := QuerySystems(client, q)
if err != nil {
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
}
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)
// 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
}
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
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx)
@ -385,7 +364,7 @@ func QueryInventory(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byt
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))
client.Registry.FilterForCompatible(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
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx)
@ -451,7 +430,7 @@ func QueryUsers(client *bmclib.Client, l *log.Logger, q *QueryParams) ([]byte, e
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)
// if err != nil {
// 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
}
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)
if err != nil {
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 {
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)
}
data := map[string]any{"Interfaces": interfaces}
data := map[string]any{"EthernetInterfaces": interfaces}
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
// if q.Verbose {
// fmt.Printf("%v\n", string(b))
// }
return b, nil
}
@ -541,7 +533,7 @@ func QueryStorage(q *QueryParams) ([]byte, error) {
return b, nil
}
func QuerySystems(q *QueryParams) ([]byte, error) {
func QuerySystems(client *bmclib.Client, q *QueryParams) ([]byte, error) {
c, err := connectGofish(q)
if err != nil {
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()
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, "", " ")
if err != nil {
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) {
baseUrl := "https://"
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"
url := baseRedfishUrl(q) + "/Systems"
res, body, err := util.MakeRequest(url, "GET", nil, nil)
if err != nil {
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
for _, member := range members {
var oid = member["@odata.id"].(string)
var infoUrl = baseUrl + oid
var infoUrl = url + oid
res, _, err := util.MakeRequest(infoUrl, "GET", nil, nil)
if err != nil {
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) {
config := makeGofishConfig(q)
c, err := gofish.Connect(config)
c.Service.ProtocolFeaturesSupported = gofish.ProtocolFeaturesSupported{
ExpandQuery: gofish.Expand{
ExpandAll: true,
Links: true,
},
if err != nil {
return nil, fmt.Errorf("could not connect to redfish endpoint: %v", err)
}
if c != nil {
c.Service.ProtocolFeaturesSupported = gofish.ProtocolFeaturesSupported{
ExpandQuery: gofish.Expand{
ExpandAll: true,
Links: true,
},
}
}
return c, err
}
func makeGofishConfig(q *QueryParams) gofish.ClientConfig {
url := "https://"
if q.User != "" && q.Pass != "" {
url += fmt.Sprintf("%s:%s@", q.User, q.Pass)
}
url += fmt.Sprintf("%s:%d", q.Host, q.Port)
url := baseRedfishUrl(q)
return gofish.ClientConfig{
Endpoint: url,
Username: q.User,
@ -691,3 +694,11 @@ func makeJson(object any) ([]byte, error) {
}
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"
)
func InsertProbeResults(path string, states *[]magellan.BMCProbeResult) error {
if states == nil {
return fmt.Errorf("states == nil")
}
// create database if it doesn't already exist
func CreateProbeResultsIfNotExists(path string) (*sqlx.DB, error) {
schema := `
CREATE TABLE IF NOT EXISTS magellan_scanned_ports (
host TEXT NOT NULL,
@ -25,9 +20,22 @@ func InsertProbeResults(path string, states *[]magellan.BMCProbeResult) error {
`
db, err := sqlx.Open("sqlite3", path)
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)
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
tx := db.MustBegin()
@ -46,13 +54,37 @@ func InsertProbeResults(path string, states *[]magellan.BMCProbeResult) error {
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)
if err != nil {
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;")
if err != nil {
return nil, fmt.Errorf("could not retrieve probes: %v", err)

View file

@ -3,14 +3,24 @@ package magellan
import (
"fmt"
"net"
"net/http"
"sync"
"time"
"github.com/bikeshack/magellan/internal/util"
)
func rawConnect(host string, ports []int, timeout int, keepOpenOnly bool) []BMCProbeResult {
results := []BMCProbeResult{}
type ScannedResult struct {
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 {
result := BMCProbeResult{
result := ScannedResult{
Host: host,
Port: p,
Protocol: "tcp",
@ -50,8 +60,8 @@ func GenerateHosts(subnet string, begin uint8, end uint8) []string {
return hosts
}
func ScanForAssets(hosts []string, ports []int, threads int, timeout int) []BMCProbeResult {
results := make([]BMCProbeResult, 0, len(hosts))
func ScanForAssets(hosts []string, ports []int, threads int, timeout int, disableProbing bool) []ScannedResult {
results := make([]ScannedResult, 0, len(hosts))
done := make(chan struct{}, threads+1)
chanHost := make(chan string, 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()
return
}
s := rawConnect(host, ports, timeout, true)
results = append(results, s...)
scannedResults := rawConnect(host, ports, timeout, true)
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) {
_, err := os.Stat(path)
if err == nil { return true, nil }
if os.IsNotExist(err) { return false, nil }
return false, err
_, err := os.Stat(path)
if err == nil { return true, nil }
if os.IsNotExist(err) { return false, nil }
return false, err
}
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}
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")
for k, v := range headers {
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) {
// get the current data + time using Go's stupid formatting
t := time.Now()
dirname := t.Format("2006-01-01 15:04:05")
final := path + "/" + dirname
// get the current data + time using Go's stupid formatting
t := time.Now()
dirname := t.Format("2006-01-01 15:04:05")
final := path + "/" + dirname
// check if path is valid and directory
pathExists, err := PathExists(final);
if err != nil {
return final, fmt.Errorf("could not check for existing path: %v", err)
}
pathExists, err := PathExists(final);
if err != nil {
return final, fmt.Errorf("could not check for existing path: %v", err)
}
if pathExists {
// make sure it is directory with 0o644 permissions
return final, fmt.Errorf("found existing path: %v", final)
}
// make sure it is directory with 0o644 permissions
return final, fmt.Errorf("found existing path: %v", final)
}
// create directory with data + time
err = os.MkdirAll(final, 0766)