Skip to content

Commit ca84ebe

Browse files
authored
Merge pull request #126 from OpenCHAMI/allend/fix-issues
Fix various issues, improve log messages, update documentation, and add regression tests
2 parents 33453e4 + 4d188cf commit ca84ebe

22 files changed

Lines changed: 875 additions & 144 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## 0.5.2
9+
10+
- Fixed issue with secrets file being closed too early
11+
- Added documentation about using `--insecure` with `scan`
12+
- Improved overall logging messages and consistency
13+
814
## 0.5.1
915

1016
### Added

README.md

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,26 @@ The `magellan` CLI tool is a Redfish-based, board management controller (BMC) di
88
<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) -->
99

1010
- [OpenCHAMI Magellan](#openchami-magellan)
11-
- [Main Features](#main-features)
12-
- [Getting Started](#getting-started)
13-
- [Building the Executable](#building-the-executable)
14-
- [Building on Debian 12 (Bookworm)](#building-on-debian-12-bookworm)
15-
- [Docker](#docker)
16-
- [Arch Linux (AUR)](#arch-linux-aur)
17-
- [Usage](#usage)
18-
- [Checking for Redfish](#checking-for-redfish)
19-
- [BMC ID Mapping](#bmc-id-mapping)
20-
- [Running the Tool](#running-the-tool)
21-
- [Managing Secrets](#managing-secrets)
22-
- [Starting the Emulator](#starting-the-emulator)
23-
- [Updating Firmware](#updating-firmware)
24-
- [Managing Power](#managing-power)
25-
- [Getting an Access Token (WIP)](#getting-an-access-token-wip)
26-
- [Running with Docker](#running-with-docker)
27-
- [How It Works](#how-it-works)
28-
- [TODO](#todo)
29-
- [Copyright](#copyright)
11+
- [Main Features](#main-features)
12+
- [Getting Started](#getting-started)
13+
- [Documentation](#documentation)
14+
- [Building the Executable](#building-the-executable)
15+
- [Building on Debian 12 (Bookworm)](#building-on-debian-12-bookworm)
16+
- [Docker](#docker)
17+
- [Arch Linux (AUR)](#arch-linux-aur)
18+
- [Usage](#usage)
19+
- [Checking for Redfish](#checking-for-redfish)
20+
- [BMC ID Mapping](#bmc-id-mapping)
21+
- [Running the Tool](#running-the-tool)
22+
- [PDU Inventory Collection](#pdu-inventory-collection)
23+
- [Starting the Emulator](#starting-the-emulator)
24+
- [Updating Firmware](#updating-firmware)
25+
- [Managing Power](#managing-power)
26+
- [Getting an Access Token (WIP)](#getting-an-access-token-wip)
27+
- [Running with Docker](#running-with-docker)
28+
- [How It Works](#how-it-works)
29+
- [TODO](#todo)
30+
- [Copyright](#copyright)
3031

3132
<!-- TOC end -->
3233

@@ -50,6 +51,20 @@ See the [TODO](#todo) section for a list of soon-ish goals planned.
5051

5152
[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 run by executing the `emulator/setup.sh` script or running `make emulator`.
5253

54+
## Documentation
55+
56+
There is detailed documentation included with the project's repository that can be built using `scdoc` and `go doc`.
57+
58+
To build the documentation, invoke the following commands.
59+
60+
```bash
61+
# man page documentation
62+
make man
63+
64+
# API reference documentation
65+
make docs
66+
```
67+
5368
## Building the Executable
5469

5570
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:
@@ -158,7 +173,6 @@ Where the `map_key` is the name of the attribute known to `magellan` that identi
158173
x<cabinet>c<chassis>s<shelf>b<blade>
159174
```
160175

161-
162176
where `<cabinet>` is a cabinet number in the cluster, `<chassis>` is a chassis within the cabinet, `<shelf>` is the shelf within the chassis and `<blade>` is the blade within a shelf where the BMC is located. The above mapping file (minus the elipsis) will work with the example described in the [Starting the Emulator](#starting-the-emulator) section.
163177

164178
If you are using `magellan` within a system deployed using RIE in the [Quickstart Deployment Recipe](https://github.com/OpenCHAMI/deployment-recipes/blob/main/quickstart/README.md) you can generate a BMC ID Map with XNAMEs that match the RIE configured XNAMEs from the RIE instances running under `docker-compose`. You can do this outside of the docker containers by running this script:
@@ -240,6 +254,9 @@ To start a network scan for BMC nodes, use the `scan` command. If the port is no
240254
--cache data/assets.db
241255
```
242256

257+
> [!NOTE]
258+
> Make sure to include the `--insecure` flag if the BMC does not require TLS verification when using HTTPS.
259+
243260
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 that are 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 with goroutines. Try using `./magellan help scan` for a complete set of options this subcommand. Alternatively, the same scan can be started using CIDR notation and with additional hosts:
244261

245262
```bash

cmd/collect.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,10 @@ var CollectCmd = &cobra.Command{
117117
params := &magellan.CollectParams{
118118
Timeout: timeout,
119119
Concurrency: concurrency,
120-
CaCertPath: cacertPath,
121120
OutputPath: outputPath,
122121
OutputDir: outputDir,
122+
Insecure: insecure,
123123
Format: collectOutputFormat,
124-
ForceUpdate: forceUpdate,
125-
AccessToken: accessToken,
126124
SecretStore: store,
127125
BMCIDMap: idMap,
128126
}
@@ -153,9 +151,8 @@ func init() {
153151
CollectCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the protocol used to query")
154152
CollectCmd.Flags().StringVarP(&outputPath, "output-file", "o", "", "Set the path to store collection data in a single file")
155153
CollectCmd.Flags().StringVarP(&outputDir, "output-dir", "O", "", "Set the path to store collection data using HIVE partitioning")
154+
CollectCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Skip TLS certificate verification during probe")
156155
CollectCmd.Flags().BoolVar(&showOutput, "show", false, "Show the output of a collect run")
157-
CollectCmd.Flags().BoolVar(&forceUpdate, "force-update", false, "Set flag to force update data sent to SMD")
158-
CollectCmd.Flags().StringVar(&cacertPath, "cacert", "", "Set the path to CA cert file (defaults to system CAs when blank)")
159156
CollectCmd.Flags().VarP(&collectOutputFormat, "format", "F", "Set the default output data format (json|yaml; can be overridden by file extensions)")
160157
CollectCmd.Flags().StringVarP(&idMap, "bmc-id-map", "m", "", "Set the BMC ID mapping from raw json data or use @<path> to specify a file path (json or yaml input)")
161158

@@ -168,8 +165,8 @@ func init() {
168165
checkBindFlagError(viper.BindPFlag("collect.protocol", CollectCmd.Flags().Lookup("protocol")))
169166
checkBindFlagError(viper.BindPFlag("collect.output-file", CollectCmd.Flags().Lookup("output-file")))
170167
checkBindFlagError(viper.BindPFlag("collect.output-dir", CollectCmd.Flags().Lookup("output-dir")))
171-
checkBindFlagError(viper.BindPFlag("collect.force-update", CollectCmd.Flags().Lookup("force-update")))
172-
checkBindFlagError(viper.BindPFlag("collect.cacert", CollectCmd.Flags().Lookup("cacert")))
168+
// checkBindFlagError(viper.BindPFlag("collect.force-update", CollectCmd.Flags().Lookup("force-update")))
169+
// checkBindFlagError(viper.BindPFlag("collect.cacert", CollectCmd.Flags().Lookup("cacert")))
173170
checkBindFlagError(viper.BindPFlags(CollectCmd.Flags()))
174171

175172
rootCmd.AddCommand(CollectCmd)

cmd/crawl.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cmd
33
import (
44
"encoding/json"
55
"fmt"
6-
"os"
76

87
"github.com/rs/zerolog/log"
98

@@ -57,7 +56,7 @@ var CrawlCmd = &cobra.Command{
5756
log.Debug().Str("uri", uri).Msgf("one or both of --username and --password NOT passed, attempting to obtain missing credentials from secret store at %s", secretsFile)
5857
if store, err = secrets.OpenStore(secretsFile); err != nil {
5958
log.Error().Str("uri", uri).Err(err).Msg("failed to open local secrets store")
60-
os.Exit(1)
59+
return
6160
}
6261

6362
// Either none of the flags were passed or only one of them were; get
@@ -137,7 +136,7 @@ var CrawlCmd = &cobra.Command{
137136
}, crawlOutputFormat)
138137
if err != nil {
139138
log.Error().Err(err).Msg("failed to marshal output JSON")
140-
os.Exit(1)
139+
return
141140
}
142141
if showOutput {
143142
fmt.Println(string(output))

cmd/root.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ var rootCmd = &cobra.Command{
6565
}
6666
},
6767
PostRun: func(cmd *cobra.Command, args []string) {
68-
log.Debug().Msg("closing log file")
68+
log.Debug().Str("path", logFile).Msg("closing log file")
6969
err := logger.LogFile.Close()
7070
if err != nil {
7171
log.Error().Err(err).Msg("failed to close log file")
@@ -147,12 +147,7 @@ func InitializeConfig() {
147147
viper.SetConfigFile(configPath)
148148
}
149149
if err := viper.ReadInConfig(); err != nil {
150-
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
151-
err = fmt.Errorf("config file not found: %w", err)
152-
} else {
153-
err = fmt.Errorf("failed to load config file: %w", err)
154-
}
155-
log.Warn().Err(err).Msg("failed to load config")
150+
log.Debug().Err(err).Msg("failed to load config")
156151
}
157152
}
158153

cmd/scan.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ var ScanCmd = &cobra.Command{
3838
Use: "scan urls...",
3939
Example: `
4040
// assumes host https://10.0.0.101:443
41-
magellan scan 10.0.0.101
41+
magellan scan 10.0.0.101 --insecure
4242
4343
// assumes subnet using HTTPS and port 443 except for specified host
4444
magellan scan http://10.0.0.101:80 https://user:password@10.0.0.102:443 http://172.16.0.105:8080 --subnet 172.16.0.0/24
@@ -47,10 +47,10 @@ var ScanCmd = &cobra.Command{
4747
magellan scan 10.0.0.101 10.0.0.102 https://172.16.0.10:443 --port 8080 --protocol tcp
4848
4949
// assumes subnet using default unspecified subnet-masks
50-
magellan scan --subnet 10.0.0.0
50+
magellan scan --subnet 10.0.0.0 -i
5151
5252
// assumes subnet using HTTPS and port 443 with specified CIDR
53-
magellan scan --subnet 10.0.0.0/16
53+
magellan scan --subnet 10.0.0.0/16 -i
5454
5555
// assumes subnet using HTTP and port 5000 similar to 192.168.0.0/16
5656
magellan scan --subnet 192.168.0.0 --protocol tcp --scheme https --port 5000 --subnet-mask 255.255.0.0
@@ -140,7 +140,6 @@ var ScanCmd = &cobra.Command{
140140
log.Trace().Any("assets", foundAssets).Msgf("found assets from scan")
141141
} else {
142142
log.Warn().Msg("no responsive assets found")
143-
// return instead of exit to close log file
144143
return
145144
}
146145

cmd/secrets.go

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ var secretsCmd = &cobra.Command{
2828
magellan secrets store $bmc_host $bmc_creds
2929
3030
// retrieve creds from secrets store
31-
magellan secrets retrieve $bmc_host -f nodes.json
31+
magellan secrets retrieve $bmc_host -f secrets.json
3232
3333
// list creds from specific secrets
3434
magellan secrets list -f nodes.json`,
@@ -43,8 +43,8 @@ var secretsGenerateKeyCmd = &cobra.Command{
4343
Run: func(cmd *cobra.Command, args []string) {
4444
key, err := secrets.GenerateMasterKey()
4545
if err != nil {
46-
fmt.Printf("Error generating master key: %v\n", err)
47-
os.Exit(1)
46+
log.Error().Err(err).Msg("failed to generate master key")
47+
return
4848
}
4949
fmt.Printf("%s\n", key)
5050
},
@@ -65,8 +65,8 @@ var secretsStoreCmd = &cobra.Command{
6565

6666
// require either the args or input file
6767
if len(args) < 1 && secretsStoreInputFile == "" {
68-
log.Error().Msg("no input data or file")
69-
os.Exit(1)
68+
log.Error().Msg("requires input data or file")
69+
return
7070
} else if len(args) > 1 && secretsStoreInputFile == "" {
7171
// use args[1] here because args[0] is the secretID
7272
secretValue = args[1]
@@ -80,6 +80,7 @@ var secretsStoreCmd = &cobra.Command{
8080
username string
8181
password string
8282
)
83+
8384
// seperate username and password provided
8485
values = strings.Split(secretValue, ":")
8586
if len(values) != 2 {
@@ -104,55 +105,76 @@ var secretsStoreCmd = &cobra.Command{
104105
case "base64": // format: ($encoded_base64_string)
105106
decoded, err := base64.StdEncoding.DecodeString(secretValue)
106107
if err != nil {
107-
log.Error().Err(err).Msg("error decoding base64 data")
108+
log.Error().
109+
Err(err).
110+
Str("path", secretsFile).
111+
Msg("failed to decode base64 data")
108112
return
109113
}
110114

111115
// check the decoded string if it's a valid JSON and has creds
112116
if !isValidCredsJSON(string(decoded)) {
113-
log.Error().Err(err).Msg("value is not a valid JSON or is missing credentials")
117+
log.Error().
118+
Err(err).
119+
Str("path", secretsFile).
120+
Msg("invalid JSON value or is missing credentials")
114121
return
115122
}
116123

117124
store, err = secrets.OpenStore(secretsFile)
118125
if err != nil {
119-
log.Error().Err(err).Msg("failed to open secrets store")
126+
log.Error().
127+
Err(err).
128+
Str("path", secretsFile).
129+
Msg("failed to open secrets store")
120130
os.Exit(1)
121131
}
122132
secretValue = string(decoded)
123133
case "json": // format: {"username": $username, "password": $password}
124134
// read input from file if set and override
125135
if secretsStoreInputFile != "" {
126136
if secretValue != "" {
127-
log.Error().Msg("cannot use -i/--input-file with positional argument")
137+
log.Error().
138+
Str("input-file", secretsStoreInputFile).
139+
Msg("cannot use -i/--input-file with positional argument")
128140
return
129141
}
130142
inputFileBytes, err = os.ReadFile(secretsStoreInputFile)
131143
if err != nil {
132-
log.Error().Err(err).Msg("failed to read input file")
144+
log.Error().
145+
Err(err).
146+
Msg("failed to read input file")
133147
return
134148
}
135149
secretValue = string(inputFileBytes)
136150
}
137151

138152
// make sure we have valid JSON with "username" and "password" properties
139153
if !isValidCredsJSON(secretValue) {
140-
log.Error().Err(err).Msg("not a valid JSON or creds")
154+
log.Error().
155+
Err(err).
156+
Msg("invalid JSON value or creds")
141157
os.Exit(1)
142158
}
143159
store, err = secrets.OpenStore(secretsFile)
144160
if err != nil {
145-
fmt.Println(err)
146-
log.Error().Err(err).Msg("failed to open secret store")
161+
log.Error().
162+
Err(err).
163+
Str("path", secretsFile).
164+
Msg("failed to open secret store")
147165
os.Exit(1)
148166
}
149167
default:
150-
log.Error().Msg("no input format set")
168+
log.Error().Msg("invalid format (see --format flag for options)")
151169
os.Exit(1)
152170
}
153171

154172
if err := store.StoreSecretByID(secretID, secretValue); err != nil {
155-
log.Error().Err(err).Msg("failed to store secret by ID")
173+
log.Error().
174+
Err(err).
175+
Str("id", secretID).
176+
Str("path", secretsFile).
177+
Msg("failed to store secret by ID")
156178
os.Exit(1)
157179
}
158180
},
@@ -224,29 +246,40 @@ var secretsListCmd = &cobra.Command{
224246
}
225247

226248
var secretsRemoveCmd = &cobra.Command{
227-
Use: "remove secretIDs...",
249+
Use: "remove secret_ids...",
228250
Args: cobra.MinimumNArgs(1),
229251
Short: "Remove secrets by IDs from secret store.",
230252
Run: func(cmd *cobra.Command, args []string) {
231253
for _, secretID := range args {
232254
// open secret store from file
233255
store, err := secrets.OpenStore(secretsFile)
234256
if err != nil {
235-
fmt.Println(err)
236-
os.Exit(1)
257+
log.Error().
258+
Err(err).
259+
Str("path", secretsFile).
260+
Msg("failed to open secret store")
261+
return
237262
}
238263

239264
// remove secret from store by it's ID
240265
err = store.RemoveSecretByID(secretID)
241266
if err != nil {
242-
fmt.Println("failed to remove secret: ", err)
243-
os.Exit(1)
267+
log.Error().
268+
Err(err).
269+
Str("id", secretID).
270+
Str("path", secretsFile).
271+
Msg("failed to remove secret")
272+
return
244273
}
245274

246275
// update store by saving to original file
247276
err = secrets.SaveSecrets(secretsFile, store.(*secrets.LocalSecretStore).Secrets)
248277
if err != nil {
249-
log.Error().Err(err).Str("path", secretsFile).Msg("failed to save secrets to file")
278+
log.Error().
279+
Err(err).
280+
Str("path", secretsFile).
281+
Msg("failed to save secrets to file")
282+
return
250283
}
251284
}
252285
},

0 commit comments

Comments
 (0)