commit b6fe201f66eadc4413a926e16f0995c71e2b1e03 Author: David J. Allen Date: Mon Aug 28 08:42:26 2023 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aac6c5e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +magellan diff --git a/README.md b/README.md new file mode 100644 index 0000000..af5e8d6 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Magellan + +Magellan is a small tool designed to collect BMC information and load the data +into `hms-smd`. It is able to probe hosts for specific open ports using the `dora` +API or it's own simplier, built-in scanner and query BMC information via `bmclib`. +Once the data is received, it is then stored into `hms-smd` using its API. + +## Building + +To build the project, run the following: + +```bash +go mod tidy && go bulid +``` + +## Usage + +Magellan can be used to load inventory components using redfish or IMPI interfaces. +It can scan subnets or specific hosts to find... + +```bash +./magellan --help +Usage of ./magellan: + --cert-pool string path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true + --driver strings set the BMC driver to use (default [redfish]) + --host strings set additional hosts + --pass string set the BMC pass (default "root_password") + --port ints set the ports to scan + --secure-tls enable secure TLS + --subnet strings set additional subnets (default [127.0.0.0]) + --threads int set the number of threads (default -1) + --timeout int set the timeout (default 1) + --user string set the BMC user (default "root") + +# example usage +magellan \ + --subnet 127.0.0.0 \ + --host 127.0.0.1 \ + --port 5000 \ + --timeout 10 \ + --user root \ + --pass root_password \ + --threads 255 \ +``` + +## TODO + +List of things left to fix or do... + +* [ ] Switch to internal scanner if `dora` fails +* [ ] Test using different `bmclib` supported drivers (mainly 'redfish') +* [ ] Confirm loading different components into `hms-smd` diff --git a/api/dora/dora.go b/api/dora/dora.go new file mode 100644 index 0000000..d571c3b --- /dev/null +++ b/api/dora/dora.go @@ -0,0 +1,74 @@ +package dora + +import ( + "davidallendj/magellan/api" + "encoding/json" + "fmt" + + "github.com/jmoiron/sqlx" +) + +const ( + Host = "http://localhost" + DbType = "sqlite3" + DbPath = "../data/assets.db" + BaseEndpoint = "/v1" + Port = 8000 +) + +type ScannedResult struct { + id string + site any + cidr string + ip string + port int + protocol string + scanner string + state string + updated string +} + +func makeEndpointUrl(endpoint string) string { + return Host + ":" + fmt.Sprint(Port) + BaseEndpoint + endpoint +} + +// Scan for BMC assets uing dora scanner +func ScanForAssets() error { + + return nil +} + +// Query dora API to get scanned ports +func QueryScannedPorts() error { + // Perform scan and collect from dora server + url := makeEndpointUrl("/scanned_ports") + _, body, err := api.MakeRequest(url, "GET", nil) + if err != nil { + return fmt.Errorf("could not discover assets: %v", err) + } + + // get data from JSON + var res map[string]any + if err := json.Unmarshal(body, &res); err != nil { + return fmt.Errorf("could not unmarshal response body: %v", err) + } + data := res["data"] + + fmt.Println(data) + + return nil +} + +// Loads scanned ports directly from DB +func LoadScannedPortsFromDB(dbPath string, dbType string) { + db, _ := sqlx.Open(dbType, dbPath) + sql := `SELECT * FROM scanned_port WHERE state='open'` + rows, _ := db.Query(sql) + for rows.Next() { + var r ScannedResult + rows.Scan( + &r.id, &r.site, &r.cidr, &r.ip, &r.port, &r.protocol, &r.scanner, + &r.state, &r.updated, + ) + } +} \ No newline at end of file diff --git a/api/smd/smd.go b/api/smd/smd.go new file mode 100644 index 0000000..d1ca571 --- /dev/null +++ b/api/smd/smd.go @@ -0,0 +1,57 @@ +package smd + +// See ref for API docs: +// https://github.com/Cray-HPE/hms-smd/blob/master/docs/examples.adoc +// https://github.com/alexlovelltroy/hms-smd +import ( + "davidallendj/magellan/api" + "fmt" +) + +const ( + Host = "http://localhost" + BaseEndpoint = "/hsm/v2" + Port = 27779 +) + +func makeEndpointUrl(endpoint string) string { + return Host + ":" + fmt.Sprint(Port) + BaseEndpoint + endpoint +} + +func GetRedfishEndpoints() error { + url := makeEndpointUrl("/Inventory/RedfishEndpoints") + _, body, err := api.MakeRequest(url, "GET", nil) + if err != nil { + return fmt.Errorf("could not get endpoint: %v", err) + } + // fmt.Println(res) + fmt.Println(string(body)) + return nil +} + +func GetComponentEndpoint(xname string) error { + url := makeEndpointUrl("/Inventory/ComponentsEndpoints/" + xname) + res, body, err := api.MakeRequest(url, "GET", nil) + if err != nil { + return fmt.Errorf("could not get endpoint: %v", err) + } + fmt.Println(res) + fmt.Println(string(body)) + return nil +} + +func AddRedfishEndpoint(inventory []byte) error { + if inventory == nil { + return fmt.Errorf("could not add redfish endpoint: no data found") + } + // Add redfish endpoint via POST `/hsm/v2/Inventory/RedfishEndpoints` endpoint + url := makeEndpointUrl("/Inventory/RedfishEndpoints") + res, body, _ := api.MakeRequest(url, "POST", inventory) + fmt.Println("res: ", res) + fmt.Println("body: ", string(body)) + return nil +} + +func UpdateRedfishEndpoint() { + // Update redfish endpoint via PUT `/hsm/v2/Inventory/RedfishEndpoints` endpoint +} \ No newline at end of file diff --git a/api/util.go b/api/util.go new file mode 100644 index 0000000..730d01b --- /dev/null +++ b/api/util.go @@ -0,0 +1,25 @@ +package api + +import ( + "bytes" + "fmt" + "io" + "net/http" +) + + +func MakeRequest(url string, httpMethod string, body []byte) (*http.Response, []byte, error) { + // url := getSmdEndpointUrl(endpoint) + req, _ := http.NewRequest(httpMethod, url, bytes.NewBuffer(body)) + req.Header.Add("User-Agent", "magellan") + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("could not make request: %v", err) + } + b, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + return nil, nil, fmt.Errorf("could not read response body: %v", err) + } + return res, b, err +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3601dd1 --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module davidallendj/magellan + +go 1.20 + +replace github.com/bmc-toolbox/dora => ../../dora + +require ( + github.com/bmc-toolbox/bmclib v0.5.7 + github.com/bombsimon/logrusr/v2 v2.0.1 + github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 + github.com/go-logr/logr v1.2.4 + github.com/jacobweinstock/registrar v0.4.7 + github.com/jmoiron/sqlx v1.3.5 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/pflag v1.0.5 +) + +require ( + github.com/bmc-toolbox/common v0.0.0-20230717121556-5eb9915a8a5a // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/stmcginnis/gofish v0.14.0 // indirect + github.com/stretchr/testify v1.8.3 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sys v0.11.0 // indirect + gopkg.in/go-playground/validator.v9 v9.31.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6c3841f --- /dev/null +++ b/go.sum @@ -0,0 +1,96 @@ +github.com/bmc-toolbox/bmclib v0.5.7 h1:v3CqOJCMUuH+kA+xi7CdY5EuzUhMH9gsBkYTQMYlbog= +github.com/bmc-toolbox/bmclib v0.5.7/go.mod h1:jSCb2/o2bZhTTg3IgShckCfFxkX4yqQC065tuYh2pKk= +github.com/bmc-toolbox/common v0.0.0-20230717121556-5eb9915a8a5a h1:SjtoU9dE3bYfYnPXODCunMztjoDgnE3DVJCPLBqwz6Q= +github.com/bmc-toolbox/common v0.0.0-20230717121556-5eb9915a8a5a/go.mod h1:SY//n1PJjZfbFbmAsB6GvEKbc7UXz3d30s3kWxfJQ/c= +github.com/bombsimon/logrusr/v2 v2.0.1 h1:1VgxVNQMCvjirZIYaT9JYn6sAVGVEcNtRE0y4mvaOAM= +github.com/bombsimon/logrusr/v2 v2.0.1/go.mod h1:ByVAX+vHdLGAfdroiMg6q0zgq2FODY2lc5YJvzmOJio= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/go-logr/logr v1.0.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/jacobweinstock/registrar v0.4.7 h1:s4dOExccgD+Pc7rJC+f3Mc3D+NXHcXUaOibtcEsPxOc= +github.com/jacobweinstock/registrar v0.4.7/go.mod h1:PWmkdGFG5/ZdCqgMo7pvB3pXABOLHc5l8oQ0sgmBNDU= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/stmcginnis/gofish v0.14.0 h1:geECNAiG33JDB2x2xDkerpOOuXFqxp5YP3EFE3vd5iM= +github.com/stmcginnis/gofish v0.14.0/go.mod h1:BLDSFTp8pDlf/xDbLZa+F7f7eW0E/CHCboggsu8CznI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= +gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/magellan.go b/internal/magellan.go new file mode 100644 index 0000000..934348d --- /dev/null +++ b/internal/magellan.go @@ -0,0 +1,338 @@ +package magellan + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "sync" + "time" + + bmclib "github.com/bmc-toolbox/bmclib" + "github.com/go-logr/logr" + "github.com/jacobweinstock/registrar" + "github.com/jmoiron/sqlx" +) + +const ( + IPMI_PORT = 623 + SSH_PORT = 22 + TLS_PORT = 443 + REDFISH_PORT = 5000 +) + +type bmcProbeResult struct { + Host string + Port int + Protocol string + State bool +} + +// NOTE: ...params were getting too long... +type QueryParams struct { + Host string + Port int + User string + Pass string + Drivers []string + Timeout int + WithSecureTLS bool + CertPoolFile 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 := []bmcProbeResult{} + 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 IF NOT EXISTS TABLE scanned_ports ( + host text, + 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 { + tx.NamedExec(`INSERT INTO scanned_ports (host, port, protocol, state) + VALUES (:Host, :Port, :Protocol, :State)`, &state) + } + 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 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 QueryInventory(l *logr.Logger, q *QueryParams) ([]byte, error) { + // discover.ScanAndConnect(url, user, pass, clientOpts) + client, err := makeClient(l, q) + if err != nil { + return nil, fmt.Errorf("could not make query: %v", err) + } + + // 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 nil, fmt.Errorf("could not open BMC client: %v", err) + } + + defer client.Close(ctx) + + inventory, err := client.Inventory(ctx) + if err != nil { + ctxCancel() + return nil, fmt.Errorf("could not get inventory: %v", err) + } + + // retrieve inventory data + b, err := json.MarshalIndent(inventory, "", " ") + if err != nil { + ctxCancel() + return nil, fmt.Errorf("could not marshal JSON: %v", err) + } + + // return b, nil + ctxCancel() + return []byte(b), nil +} + +func QueryUsers(l *logr.Logger, q *QueryParams) ([]byte, error) { + // discover.ScanAndConnect(url, user, pass, clientOpts) + client, err := makeClient(l, q) + if err != nil { + return nil, fmt.Errorf("could not make query: %v", err) + } + + // 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 nil, fmt.Errorf("could not open BMC client: %v", err) + } + + defer client.Close(ctx) + + users, err := client.ReadUsers(ctx) + if err != nil { + ctxCancel() + return nil, fmt.Errorf("could not get users: %v", err) + } + + // retrieve inventory data + b, err := json.MarshalIndent(users, "", " ") + if err != nil { + ctxCancel() + return nil, fmt.Errorf("could not marshal JSON: %v", err) + } + + // return b, nil + ctxCancel() + fmt.Printf("users: %v\n", string(b)) + return []byte(b), nil +} + +// func QueryBios(l *logr.Logger, q *QueryParams) ([]byte, error){ +// client, err := makeClient(l, q) +// if err != nil { +// return nil, fmt.Errorf("could not make query: %v", err) +// } +// return makeRequest(client, client.GetBiosConfiguration, q.Timeout) +// } + +func makeClient(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 + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + httpClient := http.Client{ + Transport: tr, + } + + // init client + clientOpts := []bmclib.Option{ + // bmclib.WithSecureTLS(), + bmclib.WithHTTPClient(&httpClient), + bmclib.WithLogger(*l), + // bmclib.WithRedfishHTTPClient(&httpClient), + // bmclib.WithRedfishPort(fmt.Sprint(q.Port)), + // bmclib.WithRedfishUseBasicAuth(true), + // bmclib.WithDellRedfishUseBasicAuth(true), + // bmclib.WithIpmitoolPort(fmt.Sprint(q.Port)), + } + + // 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) + } + // a nil pool uses the system certs + clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) + } + // url := fmt.Sprintf("https://%s:%s@%s", q.User, q.Pass, q.Host) + url := fmt.Sprintf("https://%s:%s@%s:%d", q.User, q.Pass, q.Host, q.Port) + fmt.Println("url: ", url) + client := bmclib.NewClient(url, fmt.Sprint(q.Port), q.User, q.Pass, clientOpts...) + ds := registrar.Drivers{} + for _, driver := range q.Drivers { + ds = append(ds, client.Registry.Using(driver)...) // ipmi, gofish, redfish + } + client.Registry.Drivers = ds + + return client, nil +} + +func makeRequest[T interface{}](client *bmclib.Client, fn func(context.Context) (T, error), timeout int) ([]byte, error){ + ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(timeout)) + client.Registry.FilterForCompatible(ctx) + err := client.Open(ctx) + if err != nil { + ctxCancel() + return nil, fmt.Errorf("could not open BMC client: %v", err) + } + + defer client.Close(ctx) + + response, err := fn(ctx) + if err != nil { + ctxCancel() + return nil, fmt.Errorf("could not get response: %v", err) + } + + ctxCancel() + return makeJson(response) +} + +func makeJson(object interface{}) ([]byte, error) { + b, err := json.MarshalIndent(object, "", " ") + if err != nil { + return nil, fmt.Errorf("could not marshal JSON: %v", err) + } + return []byte(b), nil +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..ac3e19d --- /dev/null +++ b/main.go @@ -0,0 +1,113 @@ +package main + +import ( + smd "davidallendj/magellan/api/smd" + magellan "davidallendj/magellan/internal" + "fmt" + + // smd "github.com/alexlovelltroy/hms-smd/pkg/redfish" + + logrusr "github.com/bombsimon/logrusr/v2" + "github.com/cznic/mathutil" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" +) + +var ( + timeout int + threads int + ports []int + subnets []string + hosts []string + withSecureTLS bool + certPoolFile string + user string + pass string + dbpath string + drivers []string +) + +// TODO: discover bmc's on network (dora) +// TODO: query bmc component information and store in db (?) +// TODO: send bmc component information to smd +func main() { + pflag.StringVar(&user, "user", "root", "set the BMC user") + pflag.StringVar(&pass, "pass", "root_password", "set the BMC pass") + pflag.StringSliceVar(&subnets, "subnet", []string{"127.0.0.0"}, "set additional subnets") + pflag.StringSliceVar(&hosts, "host", []string{}, "set additional hosts") + pflag.IntVar(&threads, "threads", -1, "set the number of threads") + pflag.IntVar(&timeout, "timeout", 1, "set the timeout") + pflag.IntSliceVar(&ports, "port", []int{}, "set the ports to scan") + pflag.StringSliceVar(&drivers, "driver", []string{"redfish"}, "set the BMC driver to use") + pflag.StringVar(&dbpath, "dbpath", ":memory:", "set the probe storage path") + pflag.BoolVar(&withSecureTLS, "secure-tls", false, "enable secure TLS") + pflag.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") + pflag.Parse() + + // make application logger + l := logrus.New() + l.Level = logrus.DebugLevel + logger := logrusr.New(l) + + // set hosts to use for scanning + hostsToScan := []string{} + if len(hosts) > 0 { + hostsToScan = hosts + } else { + for _, subnet := range subnets { + hostsToScan = append(hostsToScan, magellan.GenerateHosts(subnet, 1, 5)...) + } + } + + // set ports to use for scanning + portsToScan := []int{} + if len(ports) > 0 { + portsToScan = ports + } else { + portsToScan = append(magellan.GetDefaultPorts(), ports...) + } + + // scan and store probe data in dbPath + if threads <= 0 { + threads = mathutil.Clamp(len(hostsToScan), 1, 255) + } + probeStates := magellan.ScanForAssets(hostsToScan, portsToScan, threads, timeout) + fmt.Printf("probe states: %v\n", probeStates) + magellan.StoreStates(dbpath, &probeStates) + + // use the found results to query bmc information + inventories := [][]byte{} + for _, ps := range probeStates { + if !ps.State { + continue + } + logrus.Infof("querying bmc %v\n", ps) + q := magellan.QueryParams{ + Host: ps.Host, + Port: ps.Port, + User: user, + Pass: pass, + Drivers: drivers, + Timeout: timeout, + } + inventory, err := magellan.QueryInventory(&logger, &q) + if err != nil { + logrus.Errorf("could not query BMC information: %v\n", err) + } + inventories = append(inventories, inventory) + } + + // 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) + } +}