Merge pull request #28 from OpenCHAMI/cleanup

More clean up and updated README
This commit is contained in:
David Allen 2024-05-13 10:41:47 -06:00 committed by GitHub
commit c1e9180c56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 331 additions and 173 deletions

231
README.md
View file

@ -1,88 +1,179 @@
# Magellan # Magellan
Magellan is a board management controller discovery tool designed to scan a network The `magellan` CLI tool is a Redfish-based, board management controller (BMC) discovery tool designed to scan networks and is written in Go. The tool collects information from BMC nodes using the provided Redfish RESTful API with [`gofish`](https://github.com/stmcginnis/gofish) and loads the queried data into an [SMD](https://github.com/OpenCHAMI/smd/tree/master) instance. The tool strives to be more flexible by implementing multiple methods of discovery to work for a wider range of systems (WIP) and is capable of using independently of other tools or services.
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 ## Getting Started
Magellan is designed to do three things: [Build](#building) and [run on bare metal](#running-the-tool) or run and test with Docker using the [latest prebuilt image](#running-with-docker).
1. Scan for BMC nodes in cluster available on a network
2. Query information about each BMC node
3. Store queried information into a database
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.
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:
1. Available redfish interface with its host and port
2. A running instance of `hms-smd` with its host and port
3. Additional dependencies for `bmclib` such as `ipmitool`
## Building ## Building
Install Go, clone the repo, and then run the following in the project root: The `magellan` tool can be built to run on bare metal. Install the required Go tools, clone the repo, and then build the binary in the root directory with the following:
```bash ```bash
git clone https://github.com/bikeshack/magellan git clone https://github.com/OpenCHAMI/magellan
cd magellan cd magellan
go mod tidy && go build go mod tidy && go build
``` ```
This should find and download all of the required dependencies. Although other And that's it. The last line should find and download all of the required dependencies to build the project. Although other versions of Go may work, the project has been tested to work with versions v1.20 and later on MacOS and Linux.
versions of Go may work, the project has only been tested with v1.20.
To build the Docker container, run `docker build -t magellan:latest .` in the ### Docker
project's directory.
The tool can also run using Docker. To build the Docker container, run `docker build -t magellan:testing .` in the project's directory. This is useful if you to run `magellan` on a different system through Docker desktop without having to install and build with Go (or if you can't do so for some reason). [Prebuilt images](https://github.com/OpenCHAMI/magellan/pkgs/container/magellan) are available as well on `ghcr`. Images can be pulled directly from the repository:
```bash
docker pull ghcr.io/openchami/magellan:latest
```
See the ["Running with Docker"](#running-with-docker) section below about running with the Docker container.
## Usage ## Usage
There are three main commands to use with the tool: `scan`, `list`, and `collect`. The sections below assume that the BMC nodes have an IP address available to query Redfish. Currently, `magellan` does not support discovery with MAC addresses although that may change in the future.
To scan a network for BMC nodes, use the `scan` command. If the port is not specified,
`magellan` will probe ports 623, 442 (redfish and IPMI) by default: ### Checking for Redfish
Before using the tool, confirm that the identified node has Redfish with `curl`. Assuming the IP address for the BMC node is `172.16.0.10`, we can send a request to see if it we get a response. You might need to pass the `-k` flag if the node uses TLS or point to the appropriate certificate.
```bash ```bash
./magellan scan --subnet 192.168.0.0 --db.path data/assets.db --port 623 curl -k https://172.16.0.10/redfish/v1 --cacert cacert.pem | jq
``` ```
This will scan the `192.168.0.0` subnet returning the host and port that return a response This should return a JSON response with general information. The output below has been truncated:
and store the results in database with path `data/assets.db`. Additional flags can
be set such as `host` to add more hosts to scan not included on the subnet, `timeout` to set how long
to wait for a response from the BMC node, or `threads` to set the number of requests
to make concurrently. Try using `./magellan help scan` for a complete set of options.
To see the available BMC nodes found from the scan, use the `list` command. Make ```json
sure to point to the same database used before: {
"@odata.context": "/redfish/v1/$metadata#ServiceRoot.ServiceRoot",
"@odata.etag": "W/\"1715279084\"",
"@odata.id": "/redfish/v1/",
"@odata.type": "#ServiceRoot.v1_5_2.ServiceRoot",
"AccountService": {
"@odata.id": "/redfish/v1/AccountService"
},
"CertificateService": {
"@odata.id": "/redfish/v1/CertificateService"
},
"Chassis": {
"@odata.id": "/redfish/v1/Chassis"
},
...
}
```
To see all of the available commands, run `magellan` with the `help` subcommand:
```bash ```bash
./magellan list --db.path data/assets.db ./magellan help
Tool for BMC discovery
Usage:
magellan [flags]
magellan [command]
Available Commands:
collect Query information about BMC
completion Generate the autocompletion script for the specified shell
help Help about any command
list List information from scan
login Log in with identity provider for access token
scan Scan for BMC nodes on a network
update Update BMC node firmware
Flags:
--access-token string set the access token
-c, --config string set the config file path
--cache string set the probe storage path (default "/tmp/magellan/magellan.db")
-h, --help help for magellan
--threads int set the number of threads (default -1)
--timeout int set the timeout (default 30)
-v, --verbose set verbose flag
Use "magellan [command] --help" for more information about a command.
``` ```
This will print a list of IP address and ports found and stored from the scan. ### Running the Tool
Finally, run the `collect` command to store BMC info into `hms-smd`:
There are three main commands to use with the tool: `scan`, `list`, and `collect`. To start a network scan for BMC nodes, use the `scan` command. If the port is not specified, `magellan` will probe ports 623 and 443 by default:
```bash ```bash
./magellan collect --db.path data/assets.db --driver ipmi --timeout 5 --user admin --pass password ./magellan scan \
--subnet 172.16.0.0 \
--subnet-mask 255.255.255.0 \
--format json \
--cache data/assets.db --port 443
``` ```
This uses the info store in the database above to request information about each This will scan the `172.16.0.0` subnet returning the host and port that return a response and store the results in a local cache with at the `data/assets.db` path. Additional flags can be set such as `--host` to add more hosts to scan not included on the subnet, `--timeout` to set how long to wait for a response from the BMC node, or `--concurrency` to set the number of requests to make concurrently. Setting the `--format=json` will format the output in JSON. Try using `./magellan help scan` for a complete set of options this subcommand.
BMC node if possible. It uses the driver specified by the `driver` flag which is
passed to and set in `bmclib`. Like with the scan, the time to wait for a response
can be set with the `timeout` flag as well. This command also requires the `user`
and `pass/password` flag to be set to use `ipmitool` (which must installed as well).
Additionally, it may be necessary to set the `host` and `port` flags for `magellan`
to find the `hms-smd` API.
Note: If the `db.path` flag is not set, `magellan` will use /tmp/magellan.db by default. To inspect the cache, use the `list` command. Make sure to point to the same database used before:
```bash
./magellan list --cache data/assets.db --format json
```
This will print a list of node info found and stored from the scan. Like the `scan` subcommand, the output format can be set using the `--format` flag.
Finally, set the `MAGELLAN_ACCESS_TOKEN`run the `collect` command to query the node from cache and send the info to be stored into SMD:
```bash
./magellan collect \
--cache data/assets.db \
--timeout 5 \
--user admin \
--pass password \
--host https://example.openchami.cluster \
--port 27779 \
--output logs/
--ca-cert cacert.pem
```
This uses the info stored in cache to request information about each BMC node if possible. Like with the scan, the time to wait for a response can be set with the `--timeout` flag as well. This command also requires the `--user` and `--pass` flags to be set if access the Redfish service requires basic authentication. Additionally, it may be necessary to set the `--host` and `--port` flags for `magellan` to find the SMD API (not the root API endpoint "/hsm/v2"). The output of the `collect` can be saved by using the `--output`
Note: If the `cache` flag is not set, `magellan` will use "/tmp/$USER/magellan.db" by default.
### Updating Firmware
The `magellan` tool is capable of updating firmware with using the `update` subcommand via the Redfish API. This may sometimes necessary if some of the `collect` output is missing or is not including what is expected. The subcommand expects there to be a running HTTP/HTTPS server running that has an accessbile URL path to the firmware download. Specify the URL with the `--firmware-path` flag and the firmware type with the `--component` flag with all the other usual arguments like in the example below:
```bash
./magellan update \
--host 172.16.0.108 \
--port 443 \
--user username \ --pass password \
--firmware-path http://172.16.0.255:8005/firmware/bios/image.RBU \
--component BIOS
```
Then, the update status can be viewed by including the `--status` flag along with the other usual arguments or with the `watch` command:
```bash
./magellan update --status --host 172.16.0.110 --user admin --pass password | jq '.'
# ...or...
watch -n 1 "./magellan update --status --host 172.16.0.110 --user admin --pass password | jq '.'"
```
### Getting an Access Token (WIP)
The `magellan` tool has a `login` subcommand that works with the [`opaal`](https://github.com/OpenCHAMI/opaal) service to obtain a token needed to access the SMD service. If the SMD instance requires authentication, set the `MAGELLAN_ACCESS_TOKEN` environment variable to have `magellan` include it in the header for HTTP requests to SMD.
```bash
# must have a running OPAAL instance
./magellan login --url https://opaal:4444/login
# ...complete login flow to get token
export MAGELLAN_ACCESS_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
```
Alternatively, if you are running the OpenCHAMI quickstart, you can run the provided script to generate a token and set the environment variable that way.
```bash
quickstart_dir=path/to/deployment/recipes/quickstart
source $quickstart_dir/bash_functions.sh
export MAGELLAN_ACCESS_TOKEN=$(gen_access_token)
```
### Running with Docker
Both the `scan` and `collect` commands can be ran via Docker after pulling the image: Both the `scan` and `collect` commands can be ran via Docker after pulling the image:
@ -91,17 +182,37 @@ docker pull bikeshack/magellan:latest
docker run bikeshack/magellan:latest /magellan.sh --scan "--subnet 172.16.0.0 --port 443 --timeout 3" --collect "--user admin --pass password --host http://vm01 --port 27779" docker run bikeshack/magellan:latest /magellan.sh --scan "--subnet 172.16.0.0 --port 443 --timeout 3" --collect "--user admin --pass password --host http://vm01 --port 27779"
``` ```
## How It Works
At its core, `magellan` is designed to do three basic things:
1. Scan for BMC nodes in cluster available on a network
2. Query information about each BMC node through Redfish API
3. Store queried information into a system management database
First, the tool performs a scan to find running services on a network. This is done by sending a raw TCP packet to all specified hosts (either IP or host name) and taking note which services respond. At this point, `magellan` has no way of knowing whether this is a Redfish service or not, so another HTTP request is made to verify. Once the BMC responds with an OK status code, `magellan` will store the necessary information in a local cache database to allow collecting more information about the node later. This allows for users to only have to scan their cluster once to find systems that are currently available and scannable.
Next, the tool queries information about the BMC node using `gofish` API functions, but requires access to BMC node found in the scanning step mentioned above to work. If the node requires basic authentication, a user name and password is required to be supplied as well. Once the BMC information is retrived from each node, the info is aggregated and a HTTP request is made to a SMD instance to be stored. Optionally, the information can be written to disk for inspection and debugging purposes.
In summary, `magellan` needs at minimum the following configured to work on each node:
1. Available Redfish service with its known host and port
2. A running instance of SMD service with its known host and port
3. Docker to pull and run containers or Go to build binaries
## TODO ## TODO
List of things left to fix, do, or ideas... See the [issue list](https://github.com/OpenCHAMI/magellan/issues) for plans for `magellan`. Here is a list of other features left to add, fix, or do (and some ideas!):
* [ ] Switch to internal scanner if `dora` fails * [X] Confirm loading different components into SMD
* [ ] Set default port automatically depending on the driver used to scan
* [X] Test using different `bmclib` supported drivers (mainly 'redfish')
* [X] Confirm loading different components into `hms-smd`
* [X] Add ability to set subnet mask for scanning * [X] Add ability to set subnet mask for scanning
* [ ] Add ability to scan with other protocols like LLDP
* [ ] Add more debugging messages with the `-v/--verbose` flag
* [ ] Separate `collect` subcommand with making request to endpoint
* [X] Support logging in with `opaal` to get access token
* [X] Support using CA certificates with HTTP requests to SMD
* [ ] Add unit tests for `scan`, `list`, and `collect` commands * [ ] Add unit tests for `scan`, `list`, and `collect` commands
* [X] Clean up, remove unused, and tidy code * [ ] Clean up, remove unused, and tidy code
## Copyright ## Copyright

View file

@ -2,6 +2,7 @@ package cmd
import ( import (
"fmt" "fmt"
"os/user"
magellan "github.com/OpenCHAMI/magellan/internal" magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/OpenCHAMI/magellan/internal/api/smd" "github.com/OpenCHAMI/magellan/internal/api/smd"
@ -25,7 +26,7 @@ var collectCmd = &cobra.Command{
l := log.NewLogger(logrus.New(), logrus.DebugLevel) l := log.NewLogger(logrus.New(), logrus.DebugLevel)
// get probe states stored in db from scan // get probe states stored in db from scan
probeStates, err := sqlite.GetProbeResults(dbpath) probeStates, err := sqlite.GetProbeResults(cachePath)
if err != nil { if err != nil {
l.Log.Errorf("failed toget states: %v", err) l.Log.Errorf("failed toget states: %v", err)
} }
@ -44,17 +45,15 @@ var collectCmd = &cobra.Command{
} }
// //
if threads <= 0 { if concurrency <= 0 {
threads = mathutil.Clamp(len(probeStates), 1, 255) concurrency = mathutil.Clamp(len(probeStates), 1, 255)
} }
q := &magellan.QueryParams{ q := &magellan.QueryParams{
User: user, User: username,
Pass: pass, Pass: password,
Protocol: protocol, Protocol: protocol,
Drivers: drivers,
Preferred: preferredDriver,
Timeout: timeout, Timeout: timeout,
Threads: threads, Concurrency: concurrency,
Verbose: verbose, Verbose: verbose,
CaCertPath: cacertPath, CaCertPath: cacertPath,
OutputPath: outputPath, OutputPath: outputPath,
@ -72,17 +71,16 @@ var collectCmd = &cobra.Command{
} }
func init() { func init() {
collectCmd.PersistentFlags().StringSliceVar(&drivers, "driver", []string{"redfish"}, "set the driver(s) and fallback drivers to use") currentUser, _ = user.Current()
collectCmd.PersistentFlags().StringVar(&smd.Host, "host", smd.Host, "set the host to the smd API") collectCmd.PersistentFlags().StringVar(&smd.Host, "host", smd.Host, "set the host to the SMD API")
collectCmd.PersistentFlags().IntVarP(&smd.Port, "port", "p", smd.Port, "set the port to the smd API") collectCmd.PersistentFlags().IntVarP(&smd.Port, "port", "p", smd.Port, "set the port to the SMD API")
collectCmd.PersistentFlags().StringVar(&user, "user", "", "set the BMC user") collectCmd.PersistentFlags().StringVar(&username, "user", "", "set the BMC user")
collectCmd.PersistentFlags().StringVar(&pass, "pass", "", "set the BMC password") collectCmd.PersistentFlags().StringVar(&password, "pass", "", "set the BMC password")
collectCmd.PersistentFlags().StringVar(&protocol, "protocol", "https", "set the protocol used to query") collectCmd.PersistentFlags().StringVar(&protocol, "protocol", "https", "set the protocol used to query")
collectCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "/tmp/magellan/data/", "set the path to store collection data") collectCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", fmt.Sprintf("/tmp/%smagellan/data/", currentUser.Username+"/"), "set the path to store collection data")
collectCmd.PersistentFlags().BoolVar(&forceUpdate, "force-update", false, "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().StringVar(&cacertPath, "ca-cert", "", "path to CA cert. (defaults to system CAs)") collectCmd.PersistentFlags().StringVar(&cacertPath, "ca-cert", "", "path to CA cert. (defaults to system CAs)")
collectCmd.MarkFlagsRequiredTogether("user", "pass")
viper.BindPFlag("collect.driver", collectCmd.Flags().Lookup("driver")) viper.BindPFlag("collect.driver", collectCmd.Flags().Lookup("driver"))
viper.BindPFlag("collect.host", collectCmd.Flags().Lookup("host")) viper.BindPFlag("collect.host", collectCmd.Flags().Lookup("host"))
@ -92,10 +90,8 @@ func init() {
viper.BindPFlag("collect.protocol", collectCmd.Flags().Lookup("protocol")) viper.BindPFlag("collect.protocol", collectCmd.Flags().Lookup("protocol"))
viper.BindPFlag("collect.output", collectCmd.Flags().Lookup("output")) viper.BindPFlag("collect.output", collectCmd.Flags().Lookup("output"))
viper.BindPFlag("collect.force-update", collectCmd.Flags().Lookup("force-update")) viper.BindPFlag("collect.force-update", collectCmd.Flags().Lookup("force-update"))
viper.BindPFlag("collect.preferred-driver", collectCmd.Flags().Lookup("preferred-driver")) viper.BindPFlag("collect.ca-cert", collectCmd.Flags().Lookup("secure-tls"))
viper.BindPFlag("collect.ipmitool.path", collectCmd.Flags().Lookup("ipmitool.path")) viper.BindPFlags(collectCmd.Flags())
viper.BindPFlag("collect.secure-tls", collectCmd.Flags().Lookup("secure-tls"))
viper.BindPFlag("collect.cert-pool", collectCmd.Flags().Lookup("cert-pool"))
rootCmd.AddCommand(collectCmd) rootCmd.AddCommand(collectCmd)
} }

View file

@ -1,7 +1,9 @@
package cmd package cmd
import ( import (
"encoding/json"
"fmt" "fmt"
"strings"
"github.com/OpenCHAMI/magellan/internal/db/sqlite" "github.com/OpenCHAMI/magellan/internal/db/sqlite"
@ -13,16 +15,23 @@ var listCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List information from scan", Short: "List information from scan",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
probeResults, err := sqlite.GetProbeResults(dbpath) probeResults, err := sqlite.GetProbeResults(cachePath)
if err != nil { if err != nil {
logrus.Errorf("failed toget probe results: %v\n", err) logrus.Errorf("failed toget probe results: %v\n", err)
} }
format = strings.ToLower(format)
if format == "json" {
b, _ := json.Marshal(probeResults)
fmt.Printf("%s\n", string(b))
} else {
for _, r := range probeResults { for _, r := range probeResults {
fmt.Printf("%s:%d (%s)\n", r.Host, r.Port, r.Protocol) fmt.Printf("%s:%d (%s)\n", r.Host, r.Port, r.Protocol)
} }
}
}, },
} }
func init() { func init() {
listCmd.Flags().StringVar(&format, "format", "", "set the output format")
rootCmd.AddCommand(listCmd) rootCmd.AddCommand(listCmd)
} }

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"net" "net"
"os" "os"
"os/user"
magellan "github.com/OpenCHAMI/magellan/internal" magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/OpenCHAMI/magellan/internal/api/smd" "github.com/OpenCHAMI/magellan/internal/api/smd"
@ -12,19 +13,18 @@ import (
) )
var ( var (
currentUser *user.User
accessToken string accessToken string
format string
timeout int timeout int
threads int concurrency int
ports []int ports []int
hosts []string hosts []string
protocol string protocol string
cacertPath string cacertPath string
user string username string
pass string password string
dbpath string cachePath string
drivers []string
preferredDriver string
ipmitoolPath string
outputPath string outputPath string
configPath string configPath string
verbose bool verbose bool
@ -76,26 +76,26 @@ func LoadAccessToken() (string, error) {
} }
func init() { func init() {
currentUser, _ = user.Current()
cobra.OnInitialize(InitializeConfig) cobra.OnInitialize(InitializeConfig)
rootCmd.PersistentFlags().IntVar(&threads, "threads", -1, "set the number of threads") rootCmd.PersistentFlags().IntVar(&concurrency, "concurrency", -1, "set the number of concurrent processes")
rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 30, "set the timeout") rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 30, "set the timeout")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config file path") rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config file path")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "set verbose flag") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "set output verbosity")
rootCmd.PersistentFlags().StringVar(&accessToken, "access-token", "", "set the access token") rootCmd.PersistentFlags().StringVar(&accessToken, "access-token", "", "set the access token")
rootCmd.PersistentFlags().StringVar(&dbpath, "db.path", "/tmp/magellan/magellan.db", "set the probe storage path") rootCmd.PersistentFlags().StringVar(&cachePath, "cache", fmt.Sprintf("/tmp/%smagellan/magellan.db", currentUser.Username+"/"), "set the scanning result cache path")
// bind viper config flags with cobra // bind viper config flags with cobra
viper.BindPFlag("threads", rootCmd.Flags().Lookup("threads")) viper.BindPFlag("concurrency", rootCmd.Flags().Lookup("concurrency"))
viper.BindPFlag("timeout", rootCmd.Flags().Lookup("timeout")) viper.BindPFlag("timeout", rootCmd.Flags().Lookup("timeout"))
viper.BindPFlag("verbose", rootCmd.Flags().Lookup("verbose")) viper.BindPFlag("verbose", rootCmd.Flags().Lookup("verbose"))
viper.BindPFlag("db.path", rootCmd.Flags().Lookup("db.path")) viper.BindPFlag("cache", rootCmd.Flags().Lookup("cache"))
// viper.BindPFlags(rootCmd.Flags()) viper.BindPFlags(rootCmd.Flags())
} }
func InitializeConfig() { func InitializeConfig() {
if configPath != "" { if configPath != "" {
magellan.LoadConfig(configPath) magellan.LoadConfig(configPath)
fmt.Printf("subnets: %v\n", viper.Get("scan.subnets"))
} }
} }
@ -104,7 +104,7 @@ func SetDefaults() {
viper.SetDefault("timeout", 30) viper.SetDefault("timeout", 30)
viper.SetDefault("config", "") viper.SetDefault("config", "")
viper.SetDefault("verbose", false) viper.SetDefault("verbose", false)
viper.SetDefault("db.path", "/tmp/magellan/magellan.db") viper.SetDefault("cache", "/tmp/magellan/magellan.db")
viper.SetDefault("scan.hosts", []string{}) viper.SetDefault("scan.hosts", []string{})
viper.SetDefault("scan.ports", []int{}) viper.SetDefault("scan.ports", []int{})
viper.SetDefault("scan.subnets", []string{}) viper.SetDefault("scan.subnets", []string{})

View file

@ -1,10 +1,12 @@
package cmd package cmd
import ( import (
"encoding/json"
"fmt" "fmt"
"net" "net"
"os" "os"
"path" "path"
"strings"
magellan "github.com/OpenCHAMI/magellan/internal" magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/OpenCHAMI/magellan/internal/db/sqlite" "github.com/OpenCHAMI/magellan/internal/db/sqlite"
@ -26,61 +28,75 @@ var scanCmd = &cobra.Command{
Use: "scan", Use: "scan",
Short: "Scan for BMC nodes on a network", Short: "Scan for BMC nodes on a network",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("subnets in cmd: %v\n", subnets) var (
// set hosts to use for scanning hostsToScan []string
hostsToScan := []string{} portsToScan []int
)
// start by adding `--host` supplied to scan
if len(hosts) > 0 { if len(hosts) > 0 {
hostsToScan = hosts hostsToScan = hosts
} else {
for i, subnet := range subnets {
if len(subnet) <= 0 {
return
} }
// add hosts from `--subnets` and `--subnet-mask`
for i, subnet := range subnets {
// subnet string is empty so nothing to do here
if subnet == "" {
continue
}
// NOTE: should we check if subnet is valid here or is it done elsewhere (maybe in GenerateHosts)?
// no subnet masks supplied so add a default one for class C private networks
if len(subnetMasks) < i+1 { if len(subnetMasks) < i+1 {
subnetMasks = append(subnetMasks, net.IP{255, 255, 255, 0}) subnetMasks = append(subnetMasks, net.IP{255, 255, 255, 0})
} }
// generate a slice of all hosts to scan from subnets
hostsToScan = append(hostsToScan, magellan.GenerateHosts(subnet, &subnetMasks[i])...) hostsToScan = append(hostsToScan, magellan.GenerateHosts(subnet, &subnetMasks[i])...)
} }
}
// set ports to use for scanning // add ports to use for scanning
portsToScan := []int{}
if len(ports) > 0 { if len(ports) > 0 {
portsToScan = ports portsToScan = ports
} else { } else {
portsToScan = append(magellan.GetDefaultPorts(), ports...) // no ports supplied so only use defaults
portsToScan = magellan.GetDefaultPorts()
} }
// scan and store probe data in dbPath // scan and store scanned data in cache
if threads <= 0 { if concurrency <= 0 {
threads = mathutil.Clamp(len(hostsToScan), 1, 255) concurrency = mathutil.Clamp(len(hostsToScan), 1, 255)
} }
probeStates := magellan.ScanForAssets(hostsToScan, portsToScan, threads, timeout, disableProbing) probeStates := magellan.ScanForAssets(hostsToScan, portsToScan, concurrency, timeout, disableProbing, verbose)
if verbose { if verbose {
format = strings.ToLower(format)
if format == "json" {
b, _ := json.Marshal(probeStates)
fmt.Printf("%s\n", string(b))
} else {
for _, r := range probeStates { for _, r := range probeStates {
fmt.Printf("%s:%d (%s)\n", r.Host, r.Port, r.Protocol) fmt.Printf("%s:%d (%s)\n", r.Host, r.Port, r.Protocol)
} }
} }
}
// make the dbpath dir if needed // make the dbpath dir if needed
err := os.MkdirAll(path.Dir(dbpath), 0766) err := os.MkdirAll(path.Dir(cachePath), 0766)
if err != nil { if err != nil {
fmt.Printf("failed tomake database directory: %v", err) fmt.Printf("failed tomake database directory: %v", err)
} }
sqlite.InsertProbeResults(dbpath, &probeStates) sqlite.InsertProbeResults(cachePath, &probeStates)
}, },
} }
func init() { func init() {
scanCmd.Flags().StringSliceVar(&hosts, "host", []string{}, "set additional hosts 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().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().StringVar(&format, "format", "", "set the output format")
// scanCmd.Flags().Uint8Var(&end, "end", 255, "set the ending point for range of IP addresses")
scanCmd.Flags().StringSliceVar(&subnets, "subnet", []string{}, "set additional subnets") scanCmd.Flags().StringSliceVar(&subnets, "subnet", []string{}, "set additional subnets")
scanCmd.Flags().IPSliceVar(&subnetMasks, "subnet-mask", []net.IP{}, "set the subnet masks to use for network") scanCmd.Flags().IPSliceVar(&subnetMasks, "subnet-mask", []net.IP{}, "set the subnet masks to use for network (must match number of subnets)")
scanCmd.Flags().BoolVar(&disableProbing, "disable-probing", false, "disable probing scanned results for BMC nodes") scanCmd.Flags().BoolVar(&disableProbing, "disable-probing", false, "disable probing scanned results for BMC nodes")
viper.BindPFlag("scan.hosts", scanCmd.Flags().Lookup("host")) viper.BindPFlag("scan.hosts", scanCmd.Flags().Lookup("host"))

View file

@ -33,15 +33,15 @@ var updateCmd = &cobra.Command{
Preferred: "redfish", Preferred: "redfish",
Protocol: protocol, Protocol: protocol,
Host: host, Host: host,
User: user, User: username,
Pass: pass, Pass: password,
Timeout: timeout, Timeout: timeout,
Port: port, Port: port,
}, },
} }
// check if required params are set // check if required params are set
if host == "" || user == "" || pass == "" { if host == "" || username == "" || password == "" {
l.Log.Fatal("requires host, user, and pass to be set") l.Log.Fatal("requires host, user, and pass to be set")
} }
@ -69,8 +69,8 @@ var updateCmd = &cobra.Command{
func init() { func init() {
updateCmd.Flags().StringVar(&host, "bmc-host", "", "set the BMC host") updateCmd.Flags().StringVar(&host, "bmc-host", "", "set the BMC host")
updateCmd.Flags().IntVar(&port, "bmc-port", 443, "set the BMC port") updateCmd.Flags().IntVar(&port, "bmc-port", 443, "set the BMC port")
updateCmd.Flags().StringVar(&user, "user", "", "set the BMC user") updateCmd.Flags().StringVar(&username, "user", "", "set the BMC user")
updateCmd.Flags().StringVar(&pass, "pass", "", "set the BMC password") updateCmd.Flags().StringVar(&password, "pass", "", "set the BMC password")
updateCmd.Flags().StringVar(&transferProtocol, "transfer-protocol", "HTTP", "set the transfer protocol") updateCmd.Flags().StringVar(&transferProtocol, "transfer-protocol", "HTTP", "set the transfer protocol")
updateCmd.Flags().StringVar(&protocol, "protocol", "https", "set the Redfish protocol") updateCmd.Flags().StringVar(&protocol, "protocol", "https", "set the Redfish protocol")
updateCmd.Flags().StringVar(&firmwareUrl, "firmware-url", "", "set the path to the firmware") updateCmd.Flags().StringVar(&firmwareUrl, "firmware-url", "", "set the path to the firmware")

View file

@ -17,13 +17,7 @@ collect:
output: "/tmp/magellan/data/" output: "/tmp/magellan/data/"
threads: 1 threads: 1
force-update: false force-update: false
preferred-driver: "redfish" ca-cert: "cacert.pem"
secure-tls: false
cert-pool:
drivers:
- "redfish"
ipmitool:
path: "/usr/bin/ipmitool"
update: update:
bmc-host: bmc-host:
bmc-port: 443 bmc-port: 443
@ -36,7 +30,7 @@ update:
component: component:
secure-tls: false secure-tls: false
status: false status: false
threads: 1 concurrency: 1
timeout: 30 timeout: 30
verbose: true verbose: true
db: db:

2
go.mod
View file

@ -15,7 +15,7 @@ require (
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.17.0 github.com/spf13/viper v1.17.0
github.com/stmcginnis/gofish v0.14.0 github.com/stmcginnis/gofish v0.17.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/exp v0.0.0-20230905200255-921286631fa9
) )

2
go.sum
View file

@ -228,6 +228,8 @@ github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
github.com/stmcginnis/gofish v0.14.0 h1:geECNAiG33JDB2x2xDkerpOOuXFqxp5YP3EFE3vd5iM= 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/stmcginnis/gofish v0.14.0/go.mod h1:BLDSFTp8pDlf/xDbLZa+F7f7eW0E/CHCboggsu8CznI=
github.com/stmcginnis/gofish v0.17.0 h1:KWpxf3arkfxBFuCi01e1UYoII8UW1RmSW2ugh7f6ULk=
github.com/stmcginnis/gofish v0.17.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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

View file

@ -39,7 +39,7 @@ type QueryParams struct {
User string User string
Pass string Pass string
Drivers []string Drivers []string
Threads int Concurrency int
Preferred string Preferred string
Timeout int Timeout int
CaCertPath string CaCertPath string
@ -71,14 +71,14 @@ func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) err
offset = 0 offset = 0
wg sync.WaitGroup wg sync.WaitGroup
found = make([]string, 0, len(*probeStates)) found = make([]string, 0, len(*probeStates))
done = make(chan struct{}, q.Threads+1) done = make(chan struct{}, q.Concurrency+1)
chanProbeState = make(chan ScannedResult, q.Threads+1) chanProbeState = make(chan ScannedResult, q.Concurrency+1)
client = smd.NewClient( client = smd.NewClient(
smd.WithSecureTLS(q.CaCertPath), smd.WithSecureTLS(q.CaCertPath),
) )
) )
wg.Add(q.Threads) wg.Add(q.Concurrency)
for i := 0; i < q.Threads; i++ { for i := 0; i < q.Concurrency; i++ {
go func() { go func() {
for { for {
ps, ok := <-chanProbeState ps, ok := <-chanProbeState
@ -100,7 +100,7 @@ func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) err
gofishClient, err := connectGofish(q) gofishClient, err := connectGofish(q)
if err != nil { if err != nil {
l.Log.Errorf("failed to connect to bmc (%v:%v): %v", q.Host, q.Port, err) l.Log.Errorf("failed to connect to BMC (%v:%v): %v", q.Host, q.Port, err)
} }
// data to be sent to smd // data to be sent to smd
@ -125,7 +125,10 @@ func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) err
l.Log.Errorf("failed to query chassis: %v", err) l.Log.Errorf("failed to query chassis: %v", err)
continue continue
} }
json.Unmarshal(chassis, &rm) err = json.Unmarshal(chassis, &rm)
if err != nil {
l.Log.Errorf("failed to unmarshal chassis JSON: %v", err)
}
data["Chassis"] = rm["Chassis"] data["Chassis"] = rm["Chassis"]
// systems // systems
@ -133,15 +136,24 @@ func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) err
if err != nil { if err != nil {
l.Log.Errorf("failed to query systems: %v", err) l.Log.Errorf("failed to query systems: %v", err)
} }
json.Unmarshal(systems, &rm) err = json.Unmarshal(systems, &rm)
if err != nil {
l.Log.Errorf("failed to unmarshal system JSON: %v", err)
}
data["Systems"] = rm["Systems"] data["Systems"] = rm["Systems"]
// add other fields from systems // add other fields from systems
if len(rm["Systems"]) > 0 { if len(rm["Systems"]) > 0 {
var s map[string][]interface{} var s map[string][]interface{}
json.Unmarshal(rm["Systems"], &s) err = json.Unmarshal(rm["Systems"], &s)
if err != nil {
l.Log.Errorf("failed to unmarshal systems JSON: %v", err)
}
data["Name"] = s["Name"] data["Name"] = s["Name"]
} }
} else {
l.Log.Errorf("invalid client (client is nil)")
continue
} }
headers := make(map[string]string) headers := make(map[string]string)
@ -333,10 +345,6 @@ func CollectUsers(client *bmclib.Client, q *QueryParams) ([]byte, error) {
} }
func CollectBios(client *bmclib.Client, q *QueryParams) ([]byte, error) { func CollectBios(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// client, err := NewClient(l, q)
// if err != nil {
// return nil, fmt.Errorf("failed to make query: %v", err)
// }
b, err := makeRequest(client, client.GetBiosConfiguration, q.Timeout) b, err := makeRequest(client, client.GetBiosConfiguration, q.Timeout)
return b, err return b, err
} }
@ -347,17 +355,29 @@ func CollectEthernetInterfaces(c *gofish.APIClient, q *QueryParams, systemID str
return nil, fmt.Errorf("failed to query storage systems (%v:%v): %v", q.Host, q.Port, err) return nil, fmt.Errorf("failed to query storage systems (%v:%v): %v", q.Host, q.Port, err)
} }
var interfaces []*redfish.EthernetInterface var (
interfaces []*redfish.EthernetInterface
errList []error
)
// get all of the ethernet interfaces in our systems
for _, system := range systems { for _, system := range systems {
i, err := redfish.ListReferencedEthernetInterfaces(c, "/redfish/v1/Systems/"+system.ID+"/EthernetInterfaces/") i, err := redfish.ListReferencedEthernetInterfaces(c, "/redfish/v1/Systems/"+system.ID+"/EthernetInterfaces/")
if err != nil { if err != nil {
errList = append(errList, err)
continue continue
} }
interfaces = append(interfaces, i...) interfaces = append(interfaces, i...)
} }
if len(interfaces) <= 0 { // format the error message for printing
return nil, fmt.Errorf("failed to get ethernet interfaces: %v", err) for i, e := range errList {
err = fmt.Errorf("\t[%d] %v\n", i, e)
}
// print any report errors
if len(errList) > 0 {
return nil, fmt.Errorf("failed to get ethernet interfaces with %d errors: \n%v", len(errList), err)
} }
data := map[string]any{"EthernetInterfaces": interfaces} data := map[string]any{"EthernetInterfaces": interfaces}
@ -423,7 +443,10 @@ func CollectSystems(c *gofish.APIClient, q *QueryParams) ([]byte, error) {
continue continue
} }
var i map[string]any var i map[string]any
json.Unmarshal(interfaces, &i) err = json.Unmarshal(interfaces, &i)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal interface: %v", err)
}
temp = append(temp, map[string]any{ temp = append(temp, map[string]any{
"Data": system, "Data": system,
"EthernetInterfaces": i["EthernetInterfaces"], "EthernetInterfaces": i["EthernetInterfaces"],

View file

@ -95,11 +95,12 @@ func generateHosts(ip *net.IP, mask *net.IPMask) []string {
return hosts return hosts
} }
func ScanForAssets(hosts []string, ports []int, threads int, timeout int, disableProbing bool) []ScannedResult { func ScanForAssets(hosts []string, ports []int, threads int, timeout int, disableProbing bool, verbose bool) []ScannedResult {
results := make([]ScannedResult, 0, len(hosts)) var (
done := make(chan struct{}, threads+1) results = make([]ScannedResult, 0, len(hosts))
chanHost := make(chan string, threads+1) done = make(chan struct{}, threads+1)
// chanPort := make(chan int, threads+1) chanHost = make(chan string, threads+1)
)
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(threads) wg.Add(threads)
@ -118,8 +119,14 @@ func ScanForAssets(hosts []string, ports []int, threads int, timeout int, disabl
url := fmt.Sprintf("https://%s:%d/redfish/v1/", result.Host, result.Port) url := fmt.Sprintf("https://%s:%d/redfish/v1/", result.Host, result.Port)
res, _, err := util.MakeRequest(nil, url, "GET", nil, nil) res, _, err := util.MakeRequest(nil, url, "GET", nil, nil)
if err != nil || res == nil { if err != nil || res == nil {
if verbose {
fmt.Printf("failed to make request: %v\n", err)
}
continue continue
} else if res.StatusCode != http.StatusOK { } else if res.StatusCode != http.StatusOK {
if verbose {
fmt.Printf("request returned code: %v\n", res.StatusCode)
}
continue continue
} else { } else {
probeResults = append(probeResults, result) probeResults = append(probeResults, result)