diff --git a/README.md b/README.md index 1f12869..124f80a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/collect.go b/cmd/collect.go index e54be33..5215e1d 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -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") diff --git a/cmd/root.go b/cmd/root.go index 3fa5217..82d6b4e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,6 +12,7 @@ var ( threads int ports []int hosts []string + protocol string withSecureTLS bool certPoolFile string user string diff --git a/cmd/scan.go b/cmd/scan.go index 5f3147c..d03e734 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -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) } diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 0000000..0df2916 --- /dev/null +++ b/cmd/update.go @@ -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) +} \ No newline at end of file diff --git a/internal/api/smd/smd.go b/internal/api/smd/smd.go index 76f518b..f257ad2 100644 --- a/internal/api/smd/smd.go +++ b/internal/api/smd/smd.go @@ -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") diff --git a/internal/collect.go b/internal/collect.go index 9ca35b9..8ba7a21 100644 --- a/internal/collect.go +++ b/internal/collect.go @@ -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) +} diff --git a/internal/db/sqlite/sqlite.go b/internal/db/sqlite/sqlite.go index 39ff33b..7665f71 100644 --- a/internal/db/sqlite/sqlite.go +++ b/internal/db/sqlite/sqlite.go @@ -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) diff --git a/internal/scan.go b/internal/scan.go index 330a019..beee775 100644 --- a/internal/scan.go +++ b/internal/scan.go @@ -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...) + } + } }() } diff --git a/internal/update.go b/internal/update.go new file mode 100644 index 0000000..fbfc52e --- /dev/null +++ b/internal/update.go @@ -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 +// } \ No newline at end of file diff --git a/internal/util/util.go b/internal/util/util.go index 661d2ba..990bda9 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -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)