diff --git a/README.md b/README.md index 1f0ff4b..dec619c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Usage of ./magellan: List of things left to fix or do... * [ ] Switch to internal scanner if `dora` fails -* [ ] Test using different `bmclib` supported drivers (mainly 'redfish') +* [X] Test using different `bmclib` supported drivers (mainly 'redfish') * [ ] Confirm loading different components into `hms-smd` +* [ ] Clean up and tidy code * [ ] Add unit tests -* [ ] Code clean up and tidying diff --git a/bin/build.sh b/bin/build.sh deleted file mode 100644 index 9cff1ba..0000000 --- a/bin/build.sh +++ /dev/null @@ -1 +0,0 @@ -go mod tidy && go build -C bin/magellan \ No newline at end of file diff --git a/bin/collect.sh b/bin/collect.sh deleted file mode 100644 index 28a0aae..0000000 --- a/bin/collect.sh +++ /dev/null @@ -1 +0,0 @@ -./magellan collect --dbpath data/assets.db --driver ipmi --timeout 5 \ No newline at end of file diff --git a/bin/list.sh b/bin/list.sh deleted file mode 100644 index 09bcfcc..0000000 --- a/bin/list.sh +++ /dev/null @@ -1 +0,0 @@ -./magellan list --dbpath data/assets.db \ No newline at end of file diff --git a/bin/magellan.sh b/bin/magellan.sh new file mode 100644 index 0000000..c6c10b4 --- /dev/null +++ b/bin/magellan.sh @@ -0,0 +1,17 @@ + +function build(){ + go mod tidy && go build -C bin/magellan +} + +function scan() { + ./magellan scan --subnet 172.16.0.0 --dbpath data/assets.db --driver ipmi --port 623 +} + +function list(){ + ./magellan list --dbpath data/assets.db +} + +function collect() { + ./magellan collect --dbpath data/assets.db --driver ipmi --timeout 5 --user admin --pass password +} + diff --git a/bin/scan.sh b/bin/scan.sh deleted file mode 100644 index 450b05c..0000000 --- a/bin/scan.sh +++ /dev/null @@ -1 +0,0 @@ -./magellan scan --subnet 172.16.0.0 --dbpath data/assets.db --driver ipmi --port 623 \ No newline at end of file diff --git a/cmd/collect.go b/cmd/collect.go index 4dfcb84..c76787f 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -1,7 +1,6 @@ package cmd import ( - "davidallendj/magellan/api/smd" magellan "davidallendj/magellan/internal" "github.com/bombsimon/logrusr/v2" @@ -76,21 +75,26 @@ var collectCmd = &cobra.Command{ if err != nil { l.Errorf("could not query bios: %v\n", err) } - } - // add all endpoints to smd - for _, inventory := range inventories { - err := smd.AddRedfishEndpoint(inventory) + _, err = magellan.QueryPowerState(client, &logger, &q) if err != nil { - logrus.Errorf("could not add redfish endpoint: %v", err) + l.Errorf("could not query power state: %v\n", err) } } + // add all endpoints to smd + // for _, inventory := range inventories { + // err := smd.AddRedfishEndpoint(inventory) + // if err != nil { + // logrus.Errorf("could not add redfish endpoint: %v", err) + // } + // } + // confirm the inventories were added - err = smd.GetRedfishEndpoints() - if err != nil { - logrus.Errorf("could not get redfish endpoints: %v\n", err) - } + // err = smd.GetRedfishEndpoints() + // if err != nil { + // logrus.Errorf("could not get redfish endpoints: %v\n", err) + // } }, } diff --git a/cmd/root.go b/cmd/root.go index 8ffdfcd..86ea350 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,6 +19,7 @@ var ( pass string dbpath string drivers []string + preferredDriver string ipmitoolPath string ) @@ -53,9 +54,10 @@ func init() { rootCmd.PersistentFlags().IntVar(&threads, "threads", -1, "set the number of threads") rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 10, "set the timeout") rootCmd.PersistentFlags().IntSliceVar(&ports, "port", []int{}, "set the ports to scan") - rootCmd.PersistentFlags().StringSliceVar(&drivers, "driver", []string{"redfish"}, "set the BMC driver to use") + rootCmd.PersistentFlags().StringSliceVar(&drivers, "driver", []string{"redfish"}, "set the driver(s) and fallback drivers to use") + rootCmd.PersistentFlags().StringVar(&preferredDriver, "preferred-driver", "ipmi", "set the preferred driver to use") rootCmd.PersistentFlags().StringVar(&dbpath, "dbpath", ":memory:", "set the probe storage path") rootCmd.PersistentFlags().StringVar(&ipmitoolPath, "ipmitool", "/usr/bin/ipmitool", "set the path for ipmitool") rootCmd.PersistentFlags().BoolVar(&withSecureTLS, "secure-tls", false, "enable secure TLS") - rootCmd.PersistentFlags().StringVar(&certPoolFile, "cert-pool", "", "path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") + rootCmd.PersistentFlags().StringVar(&certPoolFile, "cert-pool", "", "path to CA cert. (defaults to system CAs; used with --secure-tls=true)") } diff --git a/internal/magellan.go b/internal/magellan.go index bd28d7d..7271628 100644 --- a/internal/magellan.go +++ b/internal/magellan.go @@ -5,15 +5,12 @@ import ( "crypto/x509" "encoding/json" "fmt" - "net" "os" - "sync" "time" bmclib "github.com/bmc-toolbox/bmclib/v2" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" - "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" _ "github.com/stmcginnis/gofish" ) @@ -47,143 +44,6 @@ type QueryParams struct { IpmitoolPath string } -func rawConnect(host string, ports []int, timeout int, keepOpenOnly bool) []bmcProbeResult { - results := []bmcProbeResult{} - for _, p := range ports { - result := bmcProbeResult{ - Host: host, - Port: p, - Protocol: "tcp", - State: false, - } - t := time.Second * time.Duration(timeout) - port := fmt.Sprint(p) - conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), t) - if err != nil { - result.State = false - // fmt.Println("Connecting error:", err) - } - if conn != nil { - result.State = true - defer conn.Close() - // fmt.Println("Opened", net.JoinHostPort(host, port)) - } - if keepOpenOnly { - if result.State { - results = append(results, result) - } - } else { - results = append(results, result) - } - } - - return results -} - -func GenerateHosts(subnet string, begin uint8, end uint8) []string { - hosts := []string{} - ip := net.ParseIP(subnet).To4() - for i := begin; i < end; i++ { - ip[3] = byte(i) - hosts = append(hosts, fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])) - } - return hosts -} - -func ScanForAssets(hosts []string, ports []int, threads int, timeout int) []bmcProbeResult { - states := make([]bmcProbeResult, 0, len(hosts)) - done := make(chan struct{}, threads+1) - chanHost := make(chan string, threads+1) - // chanPort := make(chan int, threads+1) - var wg sync.WaitGroup - wg.Add(threads) - for i := 0; i < threads; i++ { - go func() { - for { - host, ok := <-chanHost - if !ok { - wg.Done() - return - } - s := rawConnect(host, ports, timeout, true) - states = append(states, s...) - } - }() - } - - for _, host := range hosts { - chanHost <- host - } - go func() { - select { - case <-done: - wg.Done() - break - default: - time.Sleep(1000) - } - }() - close(chanHost) - wg.Wait() - close(done) - return states -} - -func StoreStates(path string, states *[]bmcProbeResult) error { - if states == nil { - return fmt.Errorf("states == nil") - } - - // create database if it doesn't already exist - schema := ` - CREATE TABLE IF NOT EXISTS magellan_scanned_ports ( - host TEXT PRIMARY KEY NOT NULL, - port INTEGER, - protocol TEXT, - state INTEGER - ); - ` - db, err := sqlx.Open("sqlite3", path) - if err != nil { - return fmt.Errorf("could not open database: %v", err) - } - db.MustExec(schema) - - // insert all probe states into db - tx := db.MustBegin() - for _, state := range *states { - sql := `INSERT OR REPLACE INTO magellan_scanned_ports (host, port, protocol, state) - VALUES (:host, :port, :protocol, :state);` - _, 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 GetStates(path string) ([]bmcProbeResult, error) { - db, err := sqlx.Open("sqlite3", path) - if err != nil { - return nil, fmt.Errorf("could not open database: %v", err) - } - - results := []bmcProbeResult{} - err = db.Select(&results, "SELECT * FROM magellan_scanned_ports ORDER BY host ASC") - if err != nil { - return nil, fmt.Errorf("could not retrieve probes: %v", err) - } - return results, nil -} - -func GetDefaultPorts() []int { - return []int{SSH_PORT, TLS_PORT, IPMI_PORT, REDFISH_PORT} -} - func NewClient(l *logr.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 @@ -312,18 +172,36 @@ func QueryInventory(client *bmclib.Client, l *logr.Logger, q *QueryParams) ([]by return []byte(b), nil } +func QueryPowerState(client *bmclib.Client, l *logr.Logger, 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) + if err != nil { + ctxCancel() + return nil, fmt.Errorf("could not open client: %v", err) + } + defer client.Close(ctx) -// func QueryInventoryV2(host string, port int, user string, pass string) ([]byte, error) { -// url := fmt.Sprintf("http://%s:%s@%s:%s/redfish/v1/", user, pass, host, fmt.Sprint(port)) -// res, body, err := api.MakeRequest(url, "GET", nil) -// if err != nil { -// return nil , fmt.Errorf("could not get endpoint: %v", err) -// } -// fmt.Println(res) -// fmt.Println(string(body)) + inventory, err := client.GetPowerState(ctx) + if err != nil { + ctxCancel() + return nil, fmt.Errorf("could not get inventory: %v", err) + } -// return body, err -// } + // retrieve inventory data + b, err := json.MarshalIndent(inventory, "", " ") + if err != nil { + ctxCancel() + return nil, fmt.Errorf("could not marshal JSON: %v", err) + } + + if q.Verbose { + fmt.Printf("power state: %v\n", string(b)) + } + ctxCancel() + return []byte(b), nil + +} func QueryUsers(client *bmclib.Client, l *logr.Logger, q *QueryParams) ([]byte, error) { // discover.ScanAndConnect(url, user, pass, clientOpts) diff --git a/internal/scan.go b/internal/scan.go new file mode 100644 index 0000000..c12bcae --- /dev/null +++ b/internal/scan.go @@ -0,0 +1,148 @@ +package magellan + +import ( + "fmt" + "net" + "sync" + "time" + + "github.com/jmoiron/sqlx" +) + + +func rawConnect(host string, ports []int, timeout int, keepOpenOnly bool) []bmcProbeResult { + results := []bmcProbeResult{} + for _, p := range ports { + result := bmcProbeResult{ + Host: host, + Port: p, + Protocol: "tcp", + State: false, + } + t := time.Second * time.Duration(timeout) + port := fmt.Sprint(p) + conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), t) + if err != nil { + result.State = false + // fmt.Println("Connecting error:", err) + } + if conn != nil { + result.State = true + defer conn.Close() + // fmt.Println("Opened", net.JoinHostPort(host, port)) + } + if keepOpenOnly { + if result.State { + results = append(results, result) + } + } else { + results = append(results, result) + } + } + + return results +} + +func GenerateHosts(subnet string, begin uint8, end uint8) []string { + hosts := []string{} + ip := net.ParseIP(subnet).To4() + for i := begin; i < end; i++ { + ip[3] = byte(i) + hosts = append(hosts, fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])) + } + return hosts +} + +func ScanForAssets(hosts []string, ports []int, threads int, timeout int) []bmcProbeResult { + states := make([]bmcProbeResult, 0, len(hosts)) + done := make(chan struct{}, threads+1) + chanHost := make(chan string, threads+1) + // chanPort := make(chan int, threads+1) + var wg sync.WaitGroup + wg.Add(threads) + for i := 0; i < threads; i++ { + go func() { + for { + host, ok := <-chanHost + if !ok { + wg.Done() + return + } + s := rawConnect(host, ports, timeout, true) + states = append(states, s...) + } + }() + } + + for _, host := range hosts { + chanHost <- host + } + go func() { + select { + case <-done: + wg.Done() + break + default: + time.Sleep(1000) + } + }() + close(chanHost) + wg.Wait() + close(done) + return states +} + +func StoreStates(path string, states *[]bmcProbeResult) error { + if states == nil { + return fmt.Errorf("states == nil") + } + + // create database if it doesn't already exist + schema := ` + CREATE TABLE IF NOT EXISTS magellan_scanned_ports ( + host TEXT PRIMARY KEY NOT NULL, + port INTEGER, + protocol TEXT, + state INTEGER + ); + ` + db, err := sqlx.Open("sqlite3", path) + if err != nil { + return fmt.Errorf("could not open database: %v", err) + } + db.MustExec(schema) + + // insert all probe states into db + tx := db.MustBegin() + for _, state := range *states { + sql := `INSERT OR REPLACE INTO magellan_scanned_ports (host, port, protocol, state) + VALUES (:host, :port, :protocol, :state);` + _, 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 GetStates(path string) ([]bmcProbeResult, error) { + db, err := sqlx.Open("sqlite3", path) + if err != nil { + return nil, fmt.Errorf("could not open database: %v", err) + } + + results := []bmcProbeResult{} + err = db.Select(&results, "SELECT * FROM magellan_scanned_ports ORDER BY host ASC") + if err != nil { + return nil, fmt.Errorf("could not retrieve probes: %v", err) + } + return results, nil +} + +func GetDefaultPorts() []int { + return []int{SSH_PORT, TLS_PORT, IPMI_PORT, REDFISH_PORT} +} \ No newline at end of file