Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API documentation to code base #44

Merged
merged 11 commits into from
Jul 23, 2024
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ COPY magellan /magellan
COPY /bin/magellan.sh /magellan.sh


CMD [ "/magellan.sh" ]
CMD [ "/magellan" ]

ENTRYPOINT [ "/sbin/tini", "--" ]
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ diff: ## git diff
git diff --exit-code
RES=$$(git status --porcelain) ; if [ -n "$$RES" ]; then echo $$RES && exit 1 ; fi

.PHONY: docs
docs: ## go docs
$(call print-target)
go doc github.com/OpenCHAMI/magellan/cmd
go doc github.com/OpenCHAMI/magellan/internal
go doc github.com/OpenCHAMI/magellan/pkg/crawler

define print-target
@printf "Executing target: \033[36m$@\033[0m\n"
Expand Down
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The `magellan` CLI tool is a Redfish-based, board management controller (BMC) di

[Build](#building) and [run on bare metal](#running-the-tool) or run and test with Docker using the [latest prebuilt image](#running-with-docker). For quick testing, the repository integrates a Redfish emulator that can be ran by executing the `emulator/setup.sh` script or running `make emulator`.

## Building
## Building the Executable

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:

Expand All @@ -18,6 +18,30 @@ go mod tidy && go build

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.

### Building on Debian 12 (Bookworm)

Getting the `magellan` tool to work with Go 1.21 on Debian 12 may require installing the `golang-1.21` meta-package from `bookworm-backports` through `apt` along with GCC for comping the `go-sqlite3` driver.

```bash
apt install gcc golang-1.21/bookworm-backport
```

The binary executable for the `golang-1.21` executable can then be found using `dpkg`.

```bash
dpkg -L golang-1.21-go
```

Using the correct binary, set the `CGO_ENABLED` environment variable and build the executable with `cgo` enabled:

```bash
export GOBIN=/usr/bin/golang-1.21/bin/go
go env -w CGO_ENABLED=1
go mod tidy && go build
```

This might take some time to complete initially because of the `go-sqlite3` driver, but should be much faster for subsequent builds.

### Docker

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:
Expand All @@ -28,6 +52,10 @@ docker pull ghcr.io/openchami/magellan:latest

See the ["Running with Docker"](#running-with-docker) section below about running with the Docker container.





## Usage

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.
Expand Down Expand Up @@ -114,7 +142,7 @@ To inspect the cache, use the `list` command. Make sure to point to the same dat

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:
Finally, set the `ACCESS_TOKEN`run the `collect` command to query the node from cache and send the info to be stored into SMD:

```bash
./magellan collect \
Expand Down Expand Up @@ -155,22 +183,22 @@ watch -n 1 "./magellan update --status --host 172.16.0.110 --user admin --pass p

### 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.
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 `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
export ACCESS_TOKEN=eyJhbGciOiJIUzI1NiIs...
```

Alternatively, if you are running the OpenCHAMI quickstart in the [deployment recipes](https://github.com/OpenCHAMI/deployment-recipes), 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)
export ACCESS_TOKEN=$(gen_access_token)
```

### Running with Docker
Expand Down
31 changes: 21 additions & 10 deletions cmd/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,17 @@ var (
forceUpdate bool
)

// The `collect` command fetches data from a collection of BMC nodes.
// This command should be ran after the `scan` to find available hosts
// on a subnet.
var collectCmd = &cobra.Command{
Use: "collect",
Short: "Query information about BMC",
Short: "Collect system information by interrogating BMC node",
Long: "Send request(s) to a collection of hosts running Redfish services found stored from the 'scan' in cache.\n" +
"See the 'scan' command on how to perform a scan.\n\n" +
"Examples:\n" +
" magellan collect --cache ./assets.db --output ./logs --timeout 30 --cacert cecert.pem\n" +
" magellan collect --host smd.example.com --port 27779 --username username --password password",
Run: func(cmd *cobra.Command, args []string) {
// make application logger
l := log.NewLogger(logrus.New(), logrus.DebugLevel)
Expand Down Expand Up @@ -49,8 +57,8 @@ var collectCmd = &cobra.Command{
concurrency = mathutil.Clamp(len(probeStates), 1, 255)
}
q := &magellan.QueryParams{
User: username,
Pass: password,
Username: username,
Password: password,
Protocol: protocol,
Timeout: timeout,
Concurrency: concurrency,
Expand All @@ -77,23 +85,26 @@ func init() {
currentUser, _ = user.Current()
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().StringVar(&username, "user", "", "set the BMC user")
collectCmd.PersistentFlags().StringVar(&password, "pass", "", "set the BMC password")
collectCmd.PersistentFlags().StringVar(&username, "username", "", "set the BMC user")
collectCmd.PersistentFlags().StringVar(&password, "password", "", "set the BMC password")
collectCmd.PersistentFlags().StringVar(&protocol, "protocol", "https", "set the protocol used to query")
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().StringVar(&cacertPath, "ca-cert", "", "path to CA cert. (defaults to system CAs)")
collectCmd.MarkFlagsRequiredTogether("user", "pass")
collectCmd.PersistentFlags().StringVar(&cacertPath, "cacert", "", "path to CA cert. (defaults to system CAs)")

// set flags to only be used together
collectCmd.MarkFlagsRequiredTogether("username", "password")

// bind flags to config properties
viper.BindPFlag("collect.driver", collectCmd.Flags().Lookup("driver"))
viper.BindPFlag("collect.host", collectCmd.Flags().Lookup("host"))
viper.BindPFlag("collect.port", collectCmd.Flags().Lookup("port"))
viper.BindPFlag("collect.user", collectCmd.Flags().Lookup("user"))
viper.BindPFlag("collect.pass", collectCmd.Flags().Lookup("pass"))
viper.BindPFlag("collect.username", collectCmd.Flags().Lookup("username"))
viper.BindPFlag("collect.password", collectCmd.Flags().Lookup("password"))
viper.BindPFlag("collect.protocol", collectCmd.Flags().Lookup("protocol"))
viper.BindPFlag("collect.output", collectCmd.Flags().Lookup("output"))
viper.BindPFlag("collect.force-update", collectCmd.Flags().Lookup("force-update"))
viper.BindPFlag("collect.ca-cert", collectCmd.Flags().Lookup("secure-tls"))
viper.BindPFlag("collect.cacert", collectCmd.Flags().Lookup("secure-tls"))
viper.BindPFlags(collectCmd.Flags())

rootCmd.AddCommand(collectCmd)
Expand Down
7 changes: 5 additions & 2 deletions cmd/crawl.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import (
"github.com/spf13/cobra"
)

// The `crawl` command walks a collection of Redfish endpoints to collect
// specfic inventory detail. This command only expects host names and does
// not require a scan to be performed beforehand.
var crawlCmd = &cobra.Command{
Use: "crawl [uri]",
Use: "crawl [uri]",
Short: "Crawl a single BMC for inventory information",
Long: "Crawl a single BMC for inventory information\n" +
"\n" +
"Example:\n" +
" magellan crawl https://bmc.example.com",
Short: "Crawl a single BMC for inventory information",
Args: func(cmd *cobra.Command, args []string) error {
// Validate that the only argument is a valid URI
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
Expand Down
10 changes: 9 additions & 1 deletion cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,17 @@ import (
"github.com/spf13/cobra"
)

// The `list` command provides an easy way to show what was found
// and stored in a cache database from a scan. The data that's stored
// is what is consumed by the `collect` command with the --cache flag.
var listCmd = &cobra.Command{
Use: "list",
Short: "List information from scan",
Short: "List information stored in cache from a scan",
Long: "Prints all of the host and associated data found from performing a scan.\n" +
"See the 'scan' command on how to perform a scan.\n\n" +
"Examples:\n" +
" magellan list\n" +
" magellan list "
Run: func(cmd *cobra.Command, args []string) {
probeResults, err := sqlite.GetProbeResults(cachePath)
if err != nil {
Expand Down
41 changes: 35 additions & 6 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// The cmd package implements the interface for the magellan CLI. The files
// contained in this package only contains implementations for handling CLI
// arguments and passing them to functions within magellan's internal API.
//
// Each CLI subcommand will have at least one corresponding internal file
// with an API routine that implements the command's functionality. The main
// API routine will usually be the first function defined in the fill.
//
// For example:
//
// cmd/scan.go --> internal/scan.go ( magellan.ScanForAssets() )
// cmd/collect.go --> internal/collect.go ( magellan.CollectAll() )
// cmd/list.go --> none (doesn't have API call since it's simple)
// cmd/update.go --> internal/update.go ( magellan.UpdateFirmware() )
package cmd

import (
Expand Down Expand Up @@ -30,11 +44,8 @@ var (
verbose bool
)

// TODO: discover bmc's on network (dora)
// TODO: query bmc component information and store in db (?)
// TODO: send bmc component information to smd
// TODO: set ports to scan automatically with set driver

// The `root` command doesn't do anything on it's own except display
// a help message and then exits.
var rootCmd = &cobra.Command{
Use: "magellan",
Short: "Tool for BMC discovery",
Expand All @@ -47,16 +58,25 @@ var rootCmd = &cobra.Command{
},
}

// This Execute() function is called from main to run the CLI.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

// LoadAccessToken() tries to load a JWT string from an environment
// variable, file, or config in that order. If loading the token
// fails with one options, it will fallback to the next option until
// all options are exhausted.
//
// Returns a token as a string with no error if successful.
// Alternatively, returns an empty string with an error if a token is
// not able to be loaded.
func LoadAccessToken() (string, error) {
// try to load token from env var
testToken := os.Getenv("MAGELLAN_ACCESS_TOKEN")
testToken := os.Getenv("ACCESS_TOKEN")
if testToken != "" {
return testToken, nil
}
Expand Down Expand Up @@ -93,12 +113,21 @@ func init() {
viper.BindPFlags(rootCmd.Flags())
}

// InitializeConfig() initializes a new config object by loading it
// from a file given a non-empty string.
//
// See the 'LoadConfig' function in 'internal/config' for details.
func InitializeConfig() {
if configPath != "" {
magellan.LoadConfig(configPath)
}
}

// SetDefaults() resets all of the viper properties back to their
// default values.
//
// TODO: This function should probably be moved to 'internal/config.go'
// instead of in this file.
func SetDefaults() {
viper.SetDefault("threads", 1)
viper.SetDefault("timeout", 30)
Expand Down
12 changes: 12 additions & 0 deletions cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,21 @@ var (
disableProbing bool
)

// The `scan` command is usually the first step to using the CLI tool.
// This command will perform a network scan over a subnet by supplying
// a list of subnets, subnet masks, and additional IP address to probe.
//
// See the `ScanForAssets()` function in 'internal/scan.go' for details
// related to the implementation.
var scanCmd = &cobra.Command{
Use: "scan",
Short: "Scan for BMC nodes on a network",
Long: "Perform a net scan by attempting to connect to each host and port specified and getting a response. " +
"If the '--disable-probe` flag is used, the tool will not send another request to probe for available " +
"Redfish services.\n\n" +
"Example:\n" +
" magellan scan --subnet 172.16.0.0/24 --add-host 10.0.0.101\n" +
" magellan scan --subnet 172.16.0.0 --subnet-mask 255.255.255.0 --cache ./assets.db",
Run: func(cmd *cobra.Command, args []string) {
var (
hostsToScan []string
Expand Down
23 changes: 15 additions & 8 deletions cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@ var (
status bool
)

// The `update` command provides an interface to easily update firmware
// using Redfish. It also provides a simple way to check the status of
// an update in-progress.
var updateCmd = &cobra.Command{
Use: "update",
Short: "Update BMC node firmware",
Long: "Perform an firmware update using Redfish by providing a remote firmware URL and component.\n" +
"Examples:\n" +
" magellan update --host 172.16.0.108 --port 443 --username bmc_username --password bmc_password --firmware-url http://172.16.0.200:8005/firmware/bios/image.RBU --component BIOS\n" +
" magellan update --status --host 172.16.0.108 --port 443 --username bmc_username --password bmc_password",
Run: func(cmd *cobra.Command, args []string) {
l := log.NewLogger(logrus.New(), logrus.DebugLevel)
q := &magellan.UpdateParams{
Expand All @@ -33,8 +40,8 @@ var updateCmd = &cobra.Command{
Preferred: "redfish",
Protocol: protocol,
Host: host,
User: username,
Pass: password,
Username: username,
Password: password,
Timeout: timeout,
Port: port,
},
Expand Down Expand Up @@ -78,14 +85,14 @@ func init() {
updateCmd.Flags().StringVar(&component, "component", "", "set the component to upgrade")
updateCmd.Flags().BoolVar(&status, "status", false, "get the status of the update")

viper.BindPFlag("bmc-host", updateCmd.Flags().Lookup("bmc-host"))
viper.BindPFlag("bmc-port", updateCmd.Flags().Lookup("bmc-port"))
viper.BindPFlag("user", updateCmd.Flags().Lookup("user"))
viper.BindPFlag("pass", updateCmd.Flags().Lookup("pass"))
viper.BindPFlag("host", updateCmd.Flags().Lookup("host"))
viper.BindPFlag("port", updateCmd.Flags().Lookup("port"))
viper.BindPFlag("username", updateCmd.Flags().Lookup("user"))
viper.BindPFlag("password", updateCmd.Flags().Lookup("pass"))
viper.BindPFlag("transfer-protocol", updateCmd.Flags().Lookup("transfer-protocol"))
viper.BindPFlag("protocol", updateCmd.Flags().Lookup("protocol"))
viper.BindPFlag("firmware-url", updateCmd.Flags().Lookup("firmware-url"))
viper.BindPFlag("firmware-version", updateCmd.Flags().Lookup("firmware-version"))
viper.BindPFlag("firmware.url", updateCmd.Flags().Lookup("firmware.url"))
viper.BindPFlag("firmware.version", updateCmd.Flags().Lookup("firmware.version"))
viper.BindPFlag("component", updateCmd.Flags().Lookup("component"))
viper.BindPFlag("secure-tls", updateCmd.Flags().Lookup("secure-tls"))
viper.BindPFlag("status", updateCmd.Flags().Lookup("status"))
Expand Down
19 changes: 10 additions & 9 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@ scan:
collect:
# host: smd-host
# port: smd-port
user: "admin"
pass: "password"
username: "admin"
password: "password"
protocol: "https"
output: "/tmp/magellan/data/"
threads: 1
force-update: false
ca-cert: "cacert.pem"
cacert: "cacert.pem"
update:
bmc-host:
bmc-port: 443
user: "admin"
pass: "password"
host:
port: 443
username: "admin"
password: "password"
transfer-protocol: "HTTP"
protocol: "https"
firmware-url:
firmware-version:
firmware:
url:
version:
component:
secure-tls: false
status: false
Expand Down
Loading