Merge branch 'main' into cache-cmd

This commit is contained in:
David Allen 2024-11-03 19:37:29 -07:00 committed by GitHub
commit 170df80621
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1118 additions and 356 deletions

View file

@ -14,21 +14,36 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.21
- name: Set up latest stable Go
uses: actions/setup-go@v5
with:
go-version: 1.21
go-version: stable
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Checkout
uses: actions/checkout@v4
with:
fetch-tags: 1
fetch-depth: 1
# Set environment variables required by GoReleaser
- name: Set build environment variables
run: |
echo "GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)" >> $GITHUB_ENV
echo "BUILD_HOST=$(hostname)" >> $GITHUB_ENV
echo "GO_VERSION=$(go version | awk '{print $3}')" >> $GITHUB_ENV
echo "BUILD_USER=$(whoami)" >> $GITHUB_ENV
echo "CGO_ENABLED=1" >> $GITHUB_ENV
- name: Docker Login
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-tags: 1
fetch-depth: 0
- name: Release with goreleaser
uses: goreleaser/goreleaser-action@v6
env:
@ -40,4 +55,4 @@ jobs:
- name: Attest Binaries
uses: actions/attest-build-provenance@v1
with:
subject-path: '${{ github.workspace }}/dist/magellan_linux_amd64_v1/magellan'
subject-path: '${{ github.workspace }}/dist/magellan_linux_amd64_v3/magellan'

44
.github/workflows/prbuild.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: Build PR with goreleaser
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, edited]
workflow_dispatch:
jobs:
prbuild:
runs-on: ubuntu-latest
steps:
- name: Set up latest stable Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Checkout
uses: actions/checkout@v4
with:
fetch-tags: 1
fetch-depth: 1
# Set environment variables required by GoReleaser
- name: Set build environment variables
run: |
echo "GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)" >> $GITHUB_ENV
echo "BUILD_HOST=$(hostname)" >> $GITHUB_ENV
echo "GO_VERSION=$(go version | awk '{print $3}')" >> $GITHUB_ENV
echo "BUILD_USER=$(whoami)" >> $GITHUB_ENV
echo "CGO_ENABLED=1" >> $GITHUB_ENV
- name: Build with goreleaser
uses: goreleaser/goreleaser-action@v6
with:
version: '~> v2'
args: release --snapshot
id: goreleaser

3
.gitignore vendored
View file

@ -4,3 +4,6 @@ emulator/rf-emulator
**.tar.gz
**.tar.zst
**.part
dist/*
**coverage.out**
magellan.1

View file

@ -4,15 +4,39 @@ before:
hooks:
- go mod download
- go install github.com/cpuguy83/go-md2man/v2@latest
- go-md2man -in README.md -out manpage.1
- go-md2man -in README.md -out magellan.1
builds:
- env:
- CGO_ENABLED=1
- binary: magellan
main: ./main.go
# export GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)
# export BUILD_HOST=$(hostname)
# export GO_VERSION=$(go version | awk '{print $3}')
# export BUILD_USER=$(whoami)
ldflags:
- "-X github.com/OpenCHAMI/magellan/internal/version.GitCommit={{ .Commit }} \
-X github.com/OpenCHAMI/magellan/internal/version.BuildTime={{ .Timestamp }} \
-X github.com/OpenCHAMI/magellan/internal/version.Version={{ .Version }} \
-X github.com/OpenCHAMI/magellan/internal/version.GitBranch={{ .Branch }} \
-X github.com/OpenCHAMI/magellan/internal/version.GitTag={{ .Tag }} \
-X github.com/OpenCHAMI/magellan/internal/version.GitState={{ .Env.GIT_STATE }} \
-X github.com/OpenCHAMI/magellan/internal/version.BuildHost={{ .Env.BUILD_HOST }} \
-X github.com/OpenCHAMI/magellan/internal/version.GoVersion={{ .Env.GO_VERSION }} \
-X github.com/OpenCHAMI/magellan/internal/version.BuildUser={{ .Env.BUILD_USER }} "
tags:
- version
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
goamd64:
- v3
env:
- CGO_ENABLED=0
archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
@ -27,17 +51,55 @@ archives:
- LICENSE
- CHANGELOG.md
- README.md
- bin/magellan.sh
- magellan.1
nfpms:
- id: magellan
formats:
- deb
- rpm
- apk
- archlinux
maintainer: "David J. Allen <allend@lanl.gov>"
description: "Magellan is a discovery tool for BMCs."
homepage: "https://www.openchami.org"
license: MIT
section: utils
priority: optional
contents:
- src: dist/magellan_{{ .Os }}_{{ if eq .Arch "amd64" }}{{ .Arch }}_{{ .Amd64 }}{{ else }}{{ .Arch }}{{ end }}/magellan
dst: /usr/local/bin/magellan
- src: magellan.1
dst: /usr/share/man/man1/
dockers:
-
image_templates:
- ghcr.io/openchami/{{.ProjectName}}:latest
- ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}
- ghcr.io/openchami/{{.ProjectName}}:v{{ .Major }}
- ghcr.io/openchami/{{.ProjectName}}:v{{ .Major }}.{{ .Minor }}
- image_templates:
- &amd64_linux_image ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}-amd64
- ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}-amd64
- ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}-amd64
use: buildx
build_flag_templates:
- "--pull"
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
goarch: amd64
goamd64: v3
extra_files:
- LICENSE
- CHANGELOG.md
- README.md
- image_templates:
- &arm64v8_linux_image ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}-arm64
- ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}-arm64
- ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}-arm64
use: buildx
build_flag_templates:
- "--pull"
- "--platform=linux/arm64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
@ -46,7 +108,32 @@ dockers:
- LICENSE
- CHANGELOG.md
- README.md
- bin/magellan.sh
goarch: arm64
docker_manifests:
- name_template: "ghcr.io/openchami/{{.ProjectName}}:latest"
image_templates:
- *amd64_linux_image
- *arm64v8_linux_image
- name_template: "ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}"
image_templates:
- *amd64_linux_image
- *arm64v8_linux_image
- name_template: "ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}"
image_templates:
- *amd64_linux_image
- *arm64v8_linux_image
- name_template: "ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}"
image_templates:
- *amd64_linux_image
- *arm64v8_linux_image
checksum:
name_template: 'checksums.txt'
snapshot:
@ -57,8 +144,3 @@ changelog:
exclude:
- '^docs:'
- '^test:'
release:
github:
name_template: "{{.Version}}"
prerelease: auto
mode: append

View file

@ -5,7 +5,265 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.5]
### Added
* Added Init() to Client interface
* Added temporary solution for creating new clients
### Changed
* Changed interface func from GetClient() to GetInternalClient()
### Fixed
* Fixed field tag in crawler
* Fixed panic when setting --cacert from invalid client
### Updated
* Updated warning message and changed SMD client to use pointer receivers
### Miscellaneous
* Merge pull request #55 from OpenCHAMI/cacert-hotfix
## [0.1.4]
### Added
* Added response body into error messages
* Added schema version to output
### Changed
* Changed collect messages to using JSON format
### Miscellaneous
* Merge branch 'main' into minor-changes
* Merge pull request #50 from OpenCHAMI/container-build
* Merge pull request #51 from OpenCHAMI/minor-changes
* Merge pull request #52 from OpenCHAMI/minor-changes
* Merge pull request #53 from OpenCHAMI/minor-changes
* Merge pull request #54 from OpenCHAMI/update-readme
* Rearranged collect error to only show when not force updating
* Updated README.md and fixed outdated info
* magellan.sh: remove unused build helper function
* release: prefix all version tags with "v"
## [0.1.3]
### Fixed
* Fixed automatic builds with docker container
* Fixed deprecation warning in goreleaser
* Fixed permissions in workflow
* Fixed typo in workflow
## [0.1.2]
### Fixed
* Fixed automatic builds with docker container
* Fixed typo in workflow
## [0.1.1]
### Added
* Added container building working
* Added more information to crawler output
### Removed
* Removed copying script in container
### Miscellaneous
* Merge pull request #49 from OpenCHAMI/add-types
## [0.1.0]
### Added
* Added TODO comments to tests and other minor change
* Added URL sanitization for SMD host and moved auth from util
* Added check for output directory for collect
* Added disclaimer about incompatibility with SMD
* Added flag to show cache info with list command and other minor changes
### Changed
* Changed 'docker' rule to 'container'
* Changed build rule and added release rule to Makefile
* Changed firmware.* back to firmware-*
* Changed host to hostname being stored in cache
* Changed how arguments are passed to update command
* Changed how based URL is derived in update functions
* Changed order of adding default ports to add host correctly
* Changed saving host to include scheme for collect
* Changed short help message for root command
* Changed showing target host to use debug instead of verbose flag
* Changed transfer-protocol flag to scheme to match other commands
* Changed the username/password flag names
### Fixed
* Fixed '--subnet' flag not adding hosts to scan
* Fixed crawl command help string
* Fixed error message format for list command
* Fixed getting ethernet interfaces in CollectEthernetInterfaces()
* Fixed imports and removed unused query params
* Fixed issue with collect requests and other minor changes
* Fixed issue with host string and added internal url package
* Fixed lint errors
* Fixed passing the correct argument in Sanitize()
* Fixed port not being added to probing request
* Fixed root persistent flags not binding correctly
* Fixed scan not probing the host correctly
* Fixed small issue with command string
* Fixed typo errors in changelog and readme
* Fixed viper flag binding in collect cmd
### Removed
* Removed 'dora' API
* Removed commented out code
* Removed extra print statement
* Removed files from util
* Removed magellan's internal logger for zerolog
* Removed storage file
* Removed unused code, rename vars, and changed output to use hive partitioning strategy
* Removed unused functions in collect.go
* Removed unused port and clarified default in README.md
* Removed unused query params
* Removed unused updating code and bmclib dependency and other minor changes
* Removed unused variables in client package
### Updated
* Updated 'cmd' package
* Updated .gitignore
* Updated Makefile to include GOPATH in some targets
* Updated README.md with features section
* Updated example config
* Updated go dependencies
* Updated tests to reflect new API changes
### Renamed
* Renamed smd package to client
* Renamed struct
* Renamed vars and switched to use zerolog
### Miscellaneous
* Minor changes and improvements
* Minor changes to fix lint errors
* Minor changes to tests
* More minor changes
* Moved SMD-related API to pkg
* Refactored how clients work to reduce hard-coded dependencies
* Refactored/reorganized utils
* Reformatted scan help message
* Separated auth from util and fixed help strings
## [0.0.20]
* Updated workflows to publish container
## [0.0.19]
### Added
* Added 'docs' rule to Makefile
* Added initial round of comments for API documentation
* Added initial tests for API and compatibiilty coverage
* Added more API documentation
* Added more documentation and changed param names
### Changed
* Changed Dockerfile to use binary instead of script
### Fixed
* Fixed issue with required param
* Fixed small typo
* Fixed syntax error with command description
### Removed
* Removed unused code that used bmclib
### Updated
* Updated README to include information about building on Debian
* Updated go dependencies removing bmclib
* Updated dependencies
### Miscellaneous
Minor changes to README.md
Tidied up CLI flag names
## [0.0.18]
### Fixed
* Fixed formatting error in workflow
## [0.0.17]
* Addressed x/net dependabot issue
## [0.0.16]
* Updated attestation path
## [0.0.15]
### Removed
* Removed unnecessary attestation support script
## [0.0.14]
* Updated to goreleaser v2
## [0.0.13]
* Updated to goreleaser v2
## [0.0.12]
* Removed attestation of non-existent container
## [0.0.11]
### Removed
* Removed docker container from goreleaser to address build errors
## [0.0.10]
* Updated .goreleaser.yaml
## [0.0.9]
* Included Checkout in workflow
## [0.0.8]
## [0.0.7]
## [0.0.6]
### Added
* Adding dev container to standardize Linux build
* Merge pull request #1 from OpenCHAMI/rehome
## [0.0.5] - 2023-11-02

View file

@ -1,13 +1,17 @@
FROM cgr.dev/chainguard/wolfi-base
FROM chainguard/wolfi-base:latest
RUN apk add --no-cache tini bash
# Include curl in the final image for manual checks of the Redfish urls
RUN set -ex \
&& apk update \
&& apk add --no-cache curl tini \
&& rm -rf /var/cache/apk/* \
&& rm -rf /tmp/*
# nobody 65534:65534
USER 65534:65534
COPY magellan /magellan
COPY /bin/magellan.sh /magellan.sh
CMD [ "/magellan" ]

View file

@ -52,14 +52,17 @@ inst: ## go install tools
$(call print-target)
go install github.com/client9/misspell/cmd/misspell@v0.3.4
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2
go install github.com/goreleaser/goreleaser@v1.18.2
go install github.com/goreleaser/goreleaser/v2@v2.3.2
go install github.com/cpuguy83/go-md2man/v2@latest
.PHONY: goreleaser
.PHONY: release
release: ## goreleaser build
$(call print-target)
$(GOPATH)/bin/goreleaser build --clean --single-target --snapshot
.PHONY: binaries
binaries: build
.PHONY: build
build: ## go build
go build -v --tags=all -ldflags=$(LDFLAGS) -o $(NAME) main.go
@ -83,7 +86,9 @@ lint: ## golangci-lint
.PHONY: test
test: ## go test
$(call print-target)
go test -race -covermode=atomic -coverprofile=coverage.out -coverpkg=./... ./...
./emulator/setup.sh &
sleep 10
go test -race -covermode=atomic -coverprofile=coverage.out -coverpkg=./... tests/api_test.go tests/compatibility_test.go
go tool cover -html=coverage.out -o coverage.html
.PHONY: diff

View file

@ -101,7 +101,7 @@ This should return a JSON response with general information. The output below ha
### Running the Tool
There are three main commands to use with the tool: `scan`, `list`, and `collect`. To see all of the available commands, run `magellan` with the `help` subcommand:
There are three main commands to use with the tool: `scan`, `list`, and `collect`. To see all of the available commands, run `magellan` with the `help` subcommand which will print this output:
```bash
Redfish-based BMC discovery tool
@ -119,16 +119,17 @@ Available Commands:
login Log in with identity provider for access token
scan Scan to discover BMC nodes on a network
update Update BMC node firmware
version Print version info and exit
Flags:
--access-token string set the access token
--cache string set the scanning result cache path (default "/tmp/allend/magellan/assets.db")
--concurrency int set the number of concurrent processes (default -1)
-c, --config string set the config file path
-d, --debug set to enable/disable debug messages
--access-token string Set the access token
--cache string Set the scanning result cache path (default "/tmp/allend/magellan/assets.db")
--concurrency int Set the number of concurrent processes (default -1)
-c, --config string Set the config file path
-d, --debug Set to enable/disable debug messages
-h, --help help for magellan
--timeout int set the timeout (default 5)
-v, --verbose set to enable/disable verbose output
--timeout int Set the timeout for requests (default 5)
-v, --verbose Set to enable/disable verbose output
Use "magellan [command] --help" for more information about a command.
```
@ -143,23 +144,19 @@ To start a network scan for BMC nodes, use the `scan` command. If the port is no
--cache data/assets.db \
```
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. Alternatively, the same scan can be started using CIDR notation and with additional hosts:
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:
```bash
./magellan scan https://10.0.0.100:5000 --subnet 172.16.0.0/24
```
Check the help for each subcommand for more examples for specifying arguments.
To inspect the cache, use the `list` command. Make sure to point to the same database used before:
Once the scan is complete, inspect the cache to see a list of found hosts with the `list` command. Make sure to point to the same database used before if you set the `--cache` flag.
```bash
./magellan list --cache data/assets.db --format json
./magellan list --cache data/assets.db
```
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 `ACCESS_TOKEN`run the `collect` command to query the node from cache and send the info to be stored into SMD:
This will print a list of host information needed for the `collect` step. Set the `ACCESS_TOKEN` if necessary and invoke `magellan` again with the `collect` subcommand to query the node BMCs stored in cache. If the `--host` flag is set, then an additional request will be made to send the output to the specified URL. The `--userame` and `--password` flags must be set if the BMC requires basic authentication.
```bash
./magellan collect \
@ -167,14 +164,14 @@ Finally, set the `ACCESS_TOKEN`run the `collect` command to query the node from
--timeout 5 \
--username $USERNAME \
--password $PASSWORD \
--host https://example.openchami.cluster:27779 \
--host https://example.openchami.cluster:8443 \
--output logs/
--cacert 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`
This will initiate a crawler that will find as much inventory data as possible. The data can be viewed from standard output by setting the `--verbose` flag. This output can also be saved by using the `--output` flag and providing a path argument.
Note: If the `cache` flag is not set, `magellan` will use "/tmp/$USER/magellan.db" by default.
Note: If the `cache` flag is not set, `magellan` will use `/tmp/$USER/magellan.db` by default.
### Updating Firmware

View file

@ -1,122 +0,0 @@
#!/bin/bash
EXE=./magellan
SUBNETS=""
PORTS=""
USER=""
PASS=""
SMD_HOST=""
SMD_PORT=""
THREADS="1"
TIMEOUT="30"
ARGS=""
FORCE_UPDATE=false
SCAN_PARAMS=""
COLLECT_PARAMS=""
function scan() {
# ./magellan scan --subnet 172.16.0.0 --port 443
${EXE} scan ${SCAN_PARAMS}
# --subnet ${SUBNETS} \
# --port ${PORTS} \
# --timeout ${TIMEOUT} \
# --threads ${THREADS}
}
function list(){
# ./magellan list
${EXE} list
}
function collect() {
# ./magellan collect --user admin --pass password
${EXE} collect ${COLLECT_PARAMS}
# --user ${USER} \
# --pass ${PASS} \
# --timeout ${TIMEOUT} \
# --threads ${THREADS} \
# --host ${SMD_HOST} \
# --port ${SMD_PORT} \
# --force-update ${FORCE_UPDATE}
}
# parse incoming arguments to set variables
while [[ $# -gt 0 ]]; do
case $1 in
--scan)
SCAN_PARAMS="$2"
shift
shift
;;
--collect)
COLLECT_PARAMS="$2"
shift
shift
;;
--subnet)
SUBNETS="$2"
shift # past argument
shift # past value
;;
-p|--port)
PORTS="$2"
shift # past argument
shift # past value
;;
--user)
USER="$2"
shift # past argument
shift # past value
;;
--pass|--password)
PASS="$2"
shift
shift
;;
--smd-host)
SMD_HOST="$2"
shift
shift
;;
--smd-port)
SMD_PORT="$2"
shift
shift
;;
--timeout)
TIMEOUT="$2"
shift
shift
;;
--threads)
THREADS="$2"
shift
shift
;;
-*|--*)
echo "Unknown option $1"
exit 1
;;
*)
ARGS+=("$1") # save positional arg
shift # past argument
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
if [[ -n $1 ]]; then
echo "Last line of file specified as non-opt/last argument:"
tail -1 "$1"
fi
scan
collect
# run with docker
# docker run magellan:latest magellan.sh \
# --scan "--subnet 127.16.0.0 --port 443" \
# --collect "--user admin --pass password --timeout 300 --threads 1 --smd-host host --smd-port port"

View file

@ -35,7 +35,7 @@ var crawlCmd = &cobra.Command{
return nil
},
Run: func(cmd *cobra.Command, args []string) {
systems, err := crawler.CrawlBMC(crawler.CrawlerConfig{
systems, err := crawler.CrawlBMCForSystems(crawler.CrawlerConfig{
URI: args[0],
Username: cmd.Flag("username").Value.String(),
Password: cmd.Flag("password").Value.String(),

View file

@ -74,13 +74,13 @@ func Execute() {
func init() {
currentUser, _ = user.Current()
cobra.OnInitialize(InitializeConfig)
rootCmd.PersistentFlags().IntVar(&concurrency, "concurrency", -1, "set the number of concurrent processes")
rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 5, "set the timeout")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config file path")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "set to enable/disable verbose output")
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "set to enable/disable debug messages")
rootCmd.PersistentFlags().StringVar(&accessToken, "access-token", "", "set the access token")
rootCmd.PersistentFlags().StringVar(&cachePath, "cache", fmt.Sprintf("/tmp/%s/magellan/assets.db", currentUser.Username), "set the scanning result cache path")
rootCmd.PersistentFlags().IntVar(&concurrency, "concurrency", -1, "Set the number of concurrent processes")
rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 5, "Set the timeout for requests")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Set the config file path")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Set to enable/disable verbose output")
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Set to enable/disable debug messages")
rootCmd.PersistentFlags().StringVar(&accessToken, "access-token", "", "Set the access token")
rootCmd.PersistentFlags().StringVar(&cachePath, "cache", fmt.Sprintf("/tmp/%s/magellan/assets.db", currentUser.Username), "Set the scanning result cache path")
// bind viper config flags with cobra
checkBindFlagError(viper.BindPFlag("concurrency", rootCmd.PersistentFlags().Lookup("concurrency")))

View file

@ -1,40 +1,18 @@
package cmd
import (
"fmt"
"github.com/OpenCHAMI/magellan/internal/version"
"github.com/spf13/cobra"
)
var (
version string
commit string
date string
output string
)
var versionCmd = &cobra.Command{
Use: "version",
Use: "version",
Short: "Print version info and exit",
Run: func(cmd *cobra.Command, args []string) {
if cmd.Flag("commit").Value.String() == "true" {
output = commit
if date != "" {
output += " built @ " + date
}
fmt.Println(output)
} else {
fmt.Println(version)
}
version.PrintVersionInfo()
},
}
func init() {
versionCmd.Flags().Bool("commit", false, "show the version commit")
rootCmd.AddCommand(versionCmd)
}
func SetVersionInfo(buildVersion string, buildCommit string, buildDate string) {
version = buildVersion
commit = buildCommit
date = buildDate
}

View file

@ -1,29 +0,0 @@
# Maintainer: David J. Allen <allend@lanl.gov>
pkgname=magellan
pkgver=v0.1.5
pkgrel=1
pkgdesc="Redfish-based BMC discovery tool written in Go"
arch=("x86_64")
url="https://github.com/OpenCHAMI/magellan"
license=('MIT')
groups=("openchami")
provides=('magellan')
conflicts=('magellan')
source_x86_64=("${url}/releases/download/${pkgver}/${pkgname}_Linux_x86_64.tar.gz")
sha256sums_x86_64=('1bb028d592d5389b519362e6aa7021f27443f0b36471e09ee7f47ab5cb6d4d7f')
# Please refer to the 'USING VCS SOURCES' section of the PKGBUILD man page for
# a description of each element in the source array.
pkgver() {
cd "$srcdir" || exit
printf "%s" "$(git describe --tags --abbrev=0)"
}
package() {
cd "$srcdir/" || exit
# install the binary to /usr/bin
mkdir -p "${pkgdir}/usr/bin"
install -m755 magellan "${pkgdir}/usr/bin/magellan"
}

1
go.mod
View file

@ -22,7 +22,6 @@ require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
)
require (

View file

@ -106,11 +106,9 @@ func DeleteScannedAssets(path string, assets ...magellan.RemoteAsset) error {
func GetScannedAssets(path string) ([]magellan.RemoteAsset, error) {
// check if path exists first to prevent creating the database
exists, err := util.PathExists(path)
_, exists := util.PathExists(path)
if !exists {
return nil, fmt.Errorf("no file found")
} else if err != nil {
return nil, err
}
// now check if the file is the SQLite database

View file

@ -10,18 +10,20 @@ import (
"net/http"
"os"
"path"
"strings"
"path/filepath"
"sync"
"time"
"github.com/OpenCHAMI/magellan/pkg/client"
"github.com/OpenCHAMI/magellan/pkg/crawler"
"github.com/OpenCHAMI/magellan/internal/util"
"github.com/rs/zerolog/log"
"github.com/Cray-HPE/hms-xname/xnames"
_ "github.com/mattn/go-sqlite3"
_ "github.com/stmcginnis/gofish"
"github.com/stmcginnis/gofish"
"github.com/stmcginnis/gofish/redfish"
"golang.org/x/exp/slices"
)
@ -41,12 +43,13 @@ type CollectParams struct {
}
// This is the main function used to collect information from the BMC nodes via Redfish.
// The results of the collect are stored in a cache specified with the `--cache` flag.
// The function expects a list of hosts found using the `ScanForAssets()` function.
//
// Requests can be made to several of the nodes using a goroutine by setting the q.Concurrency
// property value between 1 and 255.
// property value between 1 and 10000.
func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error {
// check for available probe states
// check for available remote assets found from scan
if assets == nil {
return fmt.Errorf("no assets found")
}
@ -109,14 +112,23 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error {
offset += 1
// crawl BMC node to fetch inventory data via Redfish
systems, err := crawler.CrawlBMC(crawler.CrawlerConfig{
URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port),
Username: params.Username,
Password: params.Password,
Insecure: true,
})
var (
systems []crawler.InventoryDetail
managers []crawler.Manager
config = crawler.CrawlerConfig{
URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port),
Username: params.Username,
Password: params.Password,
Insecure: true,
}
)
systems, err := crawler.CrawlBMCForSystems(config)
if err != nil {
log.Error().Err(err).Msgf("failed to crawl BMC")
log.Error().Err(err).Msg("failed to crawl BMC for systems")
}
managers, err = crawler.CrawlBMCForManagers(config)
if err != nil {
log.Error().Err(err).Msg("failed to crawl BMC for managers")
}
// data to be sent to smd
@ -129,9 +141,20 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error {
"MACRequired": true,
"RediscoverOnUpdate": false,
"Systems": systems,
"Managers": managers,
"SchemaVersion": 1,
}
// optionally, add the MACAddr property if we find a matching IP
// from the correct ethernet interface
mac, err := FindMACAddressWithIP(config, net.ParseIP(sr.Host))
if err != nil {
log.Warn().Err(err).Msgf("failed to find MAC address with IP '%s'", sr.Host)
}
if mac != "" {
data["MACAddr"] = mac
}
// create and set headers for request
headers := client.HTTPHeader{}
headers.Authorization(params.AccessToken)
@ -148,25 +171,20 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error {
// write JSON data to file if output path is set using hive partitioning strategy
if outputPath != "" {
// make directory if it does exists
exists, err := util.PathExists(outputPath)
if err == nil && !exists {
err = os.MkdirAll(outputPath, 0o644)
var (
finalPath = fmt.Sprintf("./%s/%s/%d.json", outputPath, data["ID"], time.Now().Unix())
finalDir = filepath.Dir(finalPath)
)
// if it doesn't, make the directory and write file
err = os.MkdirAll(finalDir, 0o777)
if err == nil { // no error
err = os.WriteFile(path.Clean(finalPath), body, os.ModePerm)
if err != nil {
log.Error().Err(err).Msg("failed to make directory for output")
} else {
// make the output directory to store files
outputPath, err := util.MakeOutputDirectory(outputPath, false)
if err != nil {
log.Error().Err(err).Msg("failed to make output directory")
} else {
// write the output to the final path
err = os.WriteFile(path.Clean(fmt.Sprintf("%s/%s/%d.json", params.URI, outputPath, time.Now().Unix())), body, os.ModePerm)
if err != nil {
log.Error().Err(err).Msgf("failed to write data to file")
}
}
log.Error().Err(err).Msgf("failed to write collect output to file")
}
} else { // error is set
log.Error().Err(err).Msg("failed to make directory for collect output")
}
}
@ -225,3 +243,75 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error {
return nil
}
// FindMACAddressWithIP() returns the MAC address of an ethernet interface with
// a matching IPv4Address. Returns an empty string and error if there are no matches
// found.
func FindMACAddressWithIP(config crawler.CrawlerConfig, targetIP net.IP) (string, error) {
// get the managers to find the BMC MAC address compared with IP
//
// NOTE: Since we don't have a RedfishEndpoint type abstraction in
// magellan and the crawler crawls for systems information, it
// may just make more sense to get the managers directly via
// gofish (at least for now). If there's a need for grabbing more
// manager information in the future, we can move the logic into
// the crawler.
client, err := gofish.Connect(gofish.ClientConfig{
Endpoint: config.URI,
Username: config.Username,
Password: config.Password,
Insecure: config.Insecure,
BasicAuth: true,
})
if err != nil {
if strings.HasPrefix(err.Error(), "404:") {
err = fmt.Errorf("no ServiceRoot found. This is probably not a BMC: %s", config.URI)
}
if strings.HasPrefix(err.Error(), "401:") {
err = fmt.Errorf("authentication failed. Check your username and password: %s", config.URI)
}
event := log.Error()
event.Err(err)
event.Msg("failed to connect to BMC")
return "", err
}
defer client.Logout()
var (
rf_service = client.GetService()
rf_managers []*redfish.Manager
)
rf_managers, err = rf_service.Managers()
if err != nil {
return "", fmt.Errorf("failed to get managers: %v", err)
}
// find the manager with the same IP address of the BMC to get
// it's MAC address from its EthernetInterface
for _, manager := range rf_managers {
eths, err := manager.EthernetInterfaces()
if err != nil {
log.Error().Err(err).Msgf("failed to get ethernet interfaces from manager '%s'", manager.Name)
continue
}
for _, eth := range eths {
// compare the ethernet interface IP with argument
for _, ip := range eth.IPv4Addresses {
if ip.Address == targetIP.String() {
// we found matching IP address so return the ethernet interface MAC
return eth.MACAddress, nil
}
}
// do the same thing as above, but with static IP addresses
for _, ip := range eth.IPv4StaticAddresses {
if ip.Address == targetIP.String() {
return eth.MACAddress, nil
}
}
// no matches found, so go to next ethernet interface
continue
}
}
// no matches found, so return an empty string
return "", fmt.Errorf("no ethernet interfaces found with IP address")
}

View file

@ -2,6 +2,7 @@ package util
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
@ -13,15 +14,9 @@ import (
//
// Returns whether the path exists and no error if successful,
// otherwise, it returns false with an error.
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
func PathExists(path string) (fs.FileInfo, bool) {
fi, err := os.Stat(path)
return fi, !os.IsNotExist(err)
}
// SplitPathForViper() is an utility function to split a path into 3 parts:
@ -51,17 +46,14 @@ func MakeOutputDirectory(path string, overwrite bool) (string, error) {
final := path + "/" + dirname
// check if path is valid and directory
pathExists, err := PathExists(final)
if err != nil {
return "", fmt.Errorf("failed to check for existing path: %v", err)
}
_, pathExists := PathExists(final)
if pathExists && !overwrite {
// make sure it is directory with 0o644 permissions
return "", fmt.Errorf("found existing path: %v", final)
}
// create directory with data + time
err = os.MkdirAll(final, 0766)
err := os.MkdirAll(final, 0766)
if err != nil {
return "", fmt.Errorf("failed to make directory: %v", err)
}

27
internal/util/util.go Normal file
View file

@ -0,0 +1,27 @@
package util
import (
"fmt"
"time"
)
// CheckUntil regularly check a predicate until it's true or time out is reached.
func CheckUntil(interval time.Duration, timeout time.Duration, predicate func() (bool, error)) error {
timeoutCh := time.After(timeout)
for {
select {
case <-time.After(interval):
predTrue, err := predicate()
if predTrue {
return nil
}
if err != nil {
return err
}
case <-timeoutCh:
return fmt.Errorf("timeout of %ds reached", int64(timeout/time.Second))
}
}
}

View file

@ -0,0 +1,60 @@
package version
import (
"fmt"
)
// GitCommit stores the latest Git commit hash.
// Set via -ldflags "-X github.com/OpenCHAMI/magellan/internal/version.GitCommit=$(git rev-parse HEAD)"
var GitCommit string
// BuildTime stores the build timestamp in UTC.
// Set via -ldflags "-X github.com/OpenCHAMI/magellan/internal/version.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
var BuildTime string
// Version indicates the version of the binary, such as a release number or semantic version.
// Set via -ldflags "-X github.com/OpenCHAMI/magellan/internal/version.Version=v1.0.0"
var Version string
// GitBranch holds the name of the Git branch from which the build was created.
// Set via -ldflags "-X github.com/OpenCHAMI/magellan/internal/version.GitBranch=$(git rev-parse --abbrev-ref HEAD)"
var GitBranch string
// GitTag represents the most recent Git tag at build time, if any.
// Set via -ldflags "-X github.com/OpenCHAMI/magellan/internal/version.GitTag=$(git describe --tags --abbrev=0)"
var GitTag string
// GitState indicates whether the working directory was "clean" or "dirty" (i.e., with uncommitted changes).
// Set via -ldflags "-X github.com/OpenCHAMI/magellan/internal/version.GitState=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)"
var GitState string
// BuildHost stores the hostname of the machine where the binary was built.
// Set via -ldflags "-X github.com/OpenCHAMI/magellan/internal/version.BuildHost=$(hostname)"
var BuildHost string
// GoVersion captures the Go version used to build the binary.
// Typically, this can be obtained automatically with runtime.Version(), but you can set it manually.
// Set via -ldflags "-X github.com/OpenCHAMI/magellan/internal/version.GoVersion=$(go version | awk '{print $3}')"
var GoVersion string
// BuildUser is the username of the person or system that initiated the build process.
// Set via -ldflags "-X github.com/OpenCHAMI/magellan/internal/version.BuildUser=$(whoami)"
var BuildUser string
// PrintVersionInfo outputs all versioning information for troubleshooting or version checks.
func PrintVersionInfo() {
fmt.Printf("Version: %s\n", Version)
fmt.Printf("Git Commit: %s\n", GitCommit)
fmt.Printf("Build Time: %s\n", BuildTime)
fmt.Printf("Git Branch: %s\n", GitBranch)
fmt.Printf("Git Tag: %s\n", GitTag)
fmt.Printf("Git State: %s\n", GitState)
fmt.Printf("Build Host: %s\n", BuildHost)
fmt.Printf("Go Version: %s\n", GoVersion)
fmt.Printf("Build User: %s\n", BuildUser)
}
func VersionInfo() string {
return fmt.Sprintf("Version: %s, Git Commit: %s, Build Time: %s, Git Branch: %s, Git Tag: %s, Git State: %s, Build Host: %s, Go Version: %s, Build User: %s",
Version, GitCommit, BuildTime, GitBranch, GitTag, GitState, BuildHost, GoVersion, BuildUser)
}

View file

@ -4,13 +4,6 @@ import (
"github.com/OpenCHAMI/magellan/cmd"
)
var (
version string
commit string
date string
)
func main() {
cmd.SetVersionInfo(version, commit, date)
cmd.Execute()
}

View file

@ -41,6 +41,17 @@ type NetworkInterface struct {
Adapter NetworkAdapter `json:"adapter,omitempty"` // Adapter of the interface
}
type Manager struct {
URI string `json:"uri,omitempty"`
UUID string `json:"uuid,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Model string `json:"model,omitempty"`
Type string `json:"type,omitempty"`
FirmwareVersion string `json:"firmware_version,omitempty"`
EthernetInterfaces []EthernetInterface `json:"ethernet_interfaces,omitempty"`
}
type InventoryDetail struct {
URI string `json:"uri,omitempty"` // URI of the BMC
UUID string `json:"uuid,omitempty"` // UUID of Node
@ -65,9 +76,12 @@ type InventoryDetail struct {
Chassis_Model string `json:"chassis_model,omitempty"` // Model of the Chassis
}
// CrawlBMC pulls all pertinent information from a BMC. It accepts a CrawlerConfig and returns a list of InventoryDetail structs.
func CrawlBMC(config CrawlerConfig) ([]InventoryDetail, error) {
var systems []InventoryDetail
// CrawlBMCForSystems pulls all pertinent information from a BMC. It accepts a CrawlerConfig and returns a list of InventoryDetail structs.
func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) {
var (
systems []InventoryDetail
rf_systems []*redfish.ComputerSystem
)
// initialize gofish client
client, err := gofish.Connect(gofish.ClientConfig{
Endpoint: config.URI,
@ -94,8 +108,6 @@ func CrawlBMC(config CrawlerConfig) ([]InventoryDetail, error) {
rf_service := client.GetService()
log.Info().Msgf("found ServiceRoot %s. Redfish Version %s", rf_service.ID, rf_service.RedfishVersion)
var rf_systems []*redfish.ComputerSystem
// Nodes are sometimes only found under Chassis, but they should be found under Systems.
rf_chassis, err := rf_service.Chassis()
if err == nil {
@ -114,8 +126,43 @@ func CrawlBMC(config CrawlerConfig) ([]InventoryDetail, error) {
}
log.Info().Msgf("found %d systems in ServiceRoot", len(rf_root_systems))
rf_systems = append(rf_systems, rf_root_systems...)
systems, err = walkSystems(rf_systems, nil, config.URI)
return systems, err
return walkSystems(rf_systems, nil, config.URI)
}
// CrawlBMCForSystems pulls BMC manager information.
func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) {
// initialize gofish client
var managers []Manager
client, err := gofish.Connect(gofish.ClientConfig{
Endpoint: config.URI,
Username: config.Username,
Password: config.Password,
Insecure: config.Insecure,
BasicAuth: true,
})
if err != nil {
if strings.HasPrefix(err.Error(), "404:") {
err = fmt.Errorf("no ServiceRoot found. This is probably not a BMC: %s", config.URI)
}
if strings.HasPrefix(err.Error(), "401:") {
err = fmt.Errorf("authentication failed. Check your username and password: %s", config.URI)
}
event := log.Error()
event.Err(err)
event.Msg("failed to connect to BMC")
return managers, err
}
defer client.Logout()
// Obtain the ServiceRoot
rf_service := client.GetService()
log.Info().Msgf("found ServiceRoot %s. Redfish Version %s", rf_service.ID, rf_service.RedfishVersion)
rf_managers, err := rf_service.Managers()
if err != nil {
log.Error().Err(err).Msg("failed to get managers from ServiceRoot")
}
return walkManagers(rf_managers, config.URI)
}
func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chassis, baseURI string) ([]InventoryDetail, error) {
@ -200,7 +247,44 @@ func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chass
for _, rf_trustedmodule := range rf_computersystem.TrustedModules {
system.TrustedModules = append(system.TrustedModules, fmt.Sprintf("%s %s", rf_trustedmodule.InterfaceType, rf_trustedmodule.FirmwareVersion))
}
systems = append(systems, system)
}
return systems, nil
}
func walkManagers(rf_managers []*redfish.Manager, baseURI string) ([]Manager, error) {
var managers []Manager
for _, rf_manager := range rf_managers {
rf_ethernetinterfaces, err := rf_manager.EthernetInterfaces()
if err != nil {
log.Error().Err(err).Msg("failed to get ethernet interfaces from manager")
return managers, err
}
var ethernet_interfaces []EthernetInterface
for _, rf_ethernetinterface := range rf_ethernetinterfaces {
if len(rf_ethernetinterface.IPv4Addresses) <= 0 {
continue
}
ethernet_interfaces = append(ethernet_interfaces, EthernetInterface{
URI: baseURI + rf_ethernetinterface.ODataID,
MAC: rf_ethernetinterface.MACAddress,
Name: rf_ethernetinterface.Name,
Description: rf_ethernetinterface.Description,
Enabled: rf_ethernetinterface.InterfaceEnabled,
IP: rf_ethernetinterface.IPv4Addresses[0].Address,
})
}
managers = append(managers, Manager{
URI: baseURI + "/redfish/v1/Managers/" + rf_manager.ID,
UUID: rf_manager.UUID,
Name: rf_manager.Name,
Description: rf_manager.Description,
Model: rf_manager.Model,
Type: string(rf_manager.ManagerType),
FirmwareVersion: rf_manager.FirmwareVersion,
EthernetInterfaces: ethernet_interfaces,
})
}
return managers, nil
}

View file

@ -8,59 +8,189 @@
package tests
import (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"flag"
magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/OpenCHAMI/magellan/internal/util"
"github.com/OpenCHAMI/magellan/pkg/client"
"github.com/rs/zerolog/log"
)
var (
scanParams = &magellan.ScanParams{
TargetHosts: [][]string{
[]string{
"http://127.0.0.1:443",
"http://127.0.0.1:5000",
},
},
Scheme: "https",
Protocol: "tcp",
Concurrency: 1,
Timeout: 30,
DisableProbing: false,
Verbose: false,
}
exePath = flag.String("exe", "../magellan", "path to 'magellan' binary executable")
emuPath = flag.String("emu", "./emulator/setup.sh", "path to emulator 'setup.sh' script")
)
func TestScanAndCollect(t *testing.T) {
// do a scan on the emulator cluster with probing disabled and check results
results := magellan.ScanForAssets(scanParams)
if len(results) <= 0 {
t.Fatal("expected to find at least one BMC node, but found none")
}
// do a scan on the emulator cluster with probing enabled
results = magellan.ScanForAssets(scanParams)
if len(results) <= 0 {
t.Fatal("expected to find at least one BMC node, but found none")
var (
err error
// tempDir = t.TempDir()
path string
command []string
cwd string
cmd *exec.Cmd
bufout bytes.Buffer
buferr bytes.Buffer
)
// set up the emulator to run before test
err = waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
// do a collect on the emulator cluster to collect Redfish info
err := magellan.CollectInventory(&results, &magellan.CollectParams{})
// get the current working directory and print
cwd, err = os.Getwd()
if err != nil {
log.Error().Err(err).Msg("failed to collect inventory")
t.Fatalf("failed to get working directory: %v", err)
}
fmt.Printf("cwd: %s\n", cwd)
// path, err := exec.LookPath("dexdump")
// if err != nil {
// log.Fatal(err)
// }
// try and run a "scan" with the emulator
// set up the emulator to run before test
path, err = filepath.Abs(*exePath)
if err != nil {
t.Fatalf("failed to get absolute path: %v", err)
}
command = strings.Split("scan https://127.0.0.1 --port 5000 --verbose", " ")
cmd = exec.Command(path, command...)
cmd.Stdout = &bufout
cmd.Stderr = &buferr
err = cmd.Run()
fmt.Printf("out:\n%s\nerr:\n%s\n", bufout.String(), buferr.String())
if err != nil {
t.Fatalf("failed to run 'scan' command: %v", err)
}
// make sure that the expected output is not empty
if len(buferr.Bytes()) <= 0 {
t.Fatalf("expected the 'scan' output to not be empty")
}
// try and run a "collect" with the emulator
command = strings.Split("collect --username root --password root_password --verbose", " ")
cmd = exec.Command(path, command...)
cmd.Stdout = &bufout
cmd.Stderr = &buferr
err = cmd.Run()
fmt.Printf("out:\n%s\nerr:\n%s\n", bufout.String(), buferr.String())
if err != nil {
t.Fatalf("failed to run 'collect' command: %v", err)
}
// make sure that the output is not empty
if len(bufout.Bytes()) <= 0 {
t.Fatalf("expected the 'collect' output to not be empty")
}
// TODO: check for at least one System/EthernetInterface that we know should exist
}
func TestCrawlCommand(t *testing.T) {
// TODO: add test to check the crawl command's behavior
var (
err error
command []string
cmd *exec.Cmd
bufout bytes.Buffer
buferr bytes.Buffer
path string
)
// set up the emulator to run before test
path, err = filepath.Abs(*exePath)
if err != nil {
t.Fatalf("failed to get absolute path: %v", err)
}
fmt.Printf("path: %s\n", path)
err = waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
// try and run a "collect" with the emulator
command = strings.Split("crawl --username root --password root_password -i https://127.0.0.1:5000", " ")
cmd = exec.Command(path, command...)
cmd.Stdout = &bufout
cmd.Stderr = &buferr
err = cmd.Run()
fmt.Printf("out:\n%s\nerr:\n%s\n", bufout.String(), buferr.String())
if err != nil {
t.Fatalf("failed to run 'crawl' command: %v", err)
}
// err = cmd.Wait()
// if err != nil {
// t.Fatalf("failed to call 'wait' for crawl: %v", err)
// }
// make sure that the output is not empty
if len(bufout.Bytes()) <= 0 {
t.Fatalf("expected the 'crawl' output to not be empty")
}
}
func TestListCommand(t *testing.T) {
// TODO: add test to check the list command's output
var (
err error
cmd *exec.Cmd
)
// set up the emulator to run before test
err = waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
// set up temporary directory
cmd = exec.Command("bash", "-c", fmt.Sprintf("%s list", *exePath))
err = cmd.Start()
if err != nil {
t.Fatalf("failed to run 'list' command: %v", err)
}
// NOTE: the output of `list` can be empty if no scan has been performed
}
func TestUpdateCommand(t *testing.T) {
// TODO: add test that does a Redfish simple update checking it success and
// failure points
var (
cmd *exec.Cmd
err error
)
// set up the emulator to run before test
err = waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
// set up temporary directory
cmd = exec.Command("bash", "-c", fmt.Sprintf("%s update", *exePath))
err = cmd.Start()
if err != nil {
t.Fatalf("failed to run 'update' command: %v", err)
}
}
func TestGofishFunctions(t *testing.T) {
@ -68,6 +198,115 @@ func TestGofishFunctions(t *testing.T) {
// gofish's output isn't changing spontaneously and remains predictable
}
// TestGenerateHosts() tests creating a collection of hosts by changing arguments
// and calling GenerateHostsWithSubnet().
func TestGenerateHosts(t *testing.T) {
// TODO: add test to generate hosts using a collection of subnets/masks
var (
subnet = "127.0.0.1"
subnetMask = &net.IPMask{255, 255, 255, 0}
ports = []int{443}
scheme = "https"
hosts = [][]string{}
)
t.Run("generate-hosts", func(t *testing.T) {
hosts = magellan.GenerateHostsWithSubnet(subnet, subnetMask, ports, scheme)
// check for at least one host to be generated
if len(hosts) <= 0 {
t.Fatalf("expected at least one host to be generated for subnet %s", subnet)
}
})
t.Run("generate-hosts-with-multiple-ports", func(t *testing.T) {
ports = []int{443, 5000}
hosts = magellan.GenerateHostsWithSubnet(subnet, subnetMask, ports, scheme)
// check for at least one host to be generated
if len(hosts) <= 0 {
t.Fatalf("expected at least one host to be generated for subnet %s", subnet)
}
})
t.Run("generate-hosts-with-subnet-mask", func(t *testing.T) {
subnetMask = &net.IPMask{255, 255, 0, 0}
hosts = magellan.GenerateHostsWithSubnet(subnet, subnetMask, ports, scheme)
// check for at least one host to be generated
if len(hosts) <= 0 {
t.Fatalf("expected at least one host to be generated for subnet %s", subnet)
}
})
}
func startEmulatorInBackground(path string) (int, error) {
// try and start the emulator in the background if arg passed
var (
cmd *exec.Cmd
err error
)
if path != "" {
cmd = exec.Command("bash", "-c", path)
err = cmd.Start()
if err != nil {
return -1, fmt.Errorf("failed while executing emulator startup script: %v", err)
}
} else {
return -1, fmt.Errorf("path to emulator start up script is required")
}
return cmd.Process.Pid, nil
}
// waitUntilEmulatorIsReady() polls with
func waitUntilEmulatorIsReady() error {
var (
interval = time.Second * 2
timeout = time.Second * 6
testClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
body client.HTTPBody
header client.HTTPHeader
err error
)
err = util.CheckUntil(interval, timeout, func() (bool, error) {
// send request to host until we get expected response
res, _, err := client.MakeRequest(testClient, "https://127.0.0.1:5000/redfish/v1/", http.MethodGet, body, header)
if err != nil {
return false, fmt.Errorf("failed to make request to emulator: %w", err)
}
if res == nil {
return false, fmt.Errorf("invalid response from emulator (response is nil)")
}
if res.StatusCode == http.StatusOK {
return true, nil
} else {
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
})
return err
}
func init() {
var (
cwd string
err error
)
// get the current working directory
cwd, err = os.Getwd()
if err != nil {
log.Error().Err(err).Msg("failed to get working directory")
}
fmt.Printf("cwd: %s\n", cwd)
// start emulator in the background before running tests
pid, err := startEmulatorInBackground(*emuPath)
if err != nil {
log.Error().Err(err).Msg("failed to start emulator in background")
os.Exit(1)
}
_ = pid
}

View file

@ -7,6 +7,7 @@
package tests
import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
@ -15,56 +16,97 @@ import (
"github.com/OpenCHAMI/magellan/pkg/client"
"github.com/OpenCHAMI/magellan/pkg/crawler"
"github.com/rs/zerolog/log"
)
var (
host = flag.String("host", "localhost", "set the BMC host")
username = flag.String("username", "", "set the BMC username used for the tests")
password = flag.String("password", "", "set the BMC password used for the tests")
host = flag.String("host", "https://127.0.0.1:5000", "set the BMC host")
username = flag.String("username", "root", "set the BMC username used for the tests")
password = flag.String("password", "root_password", "set the BMC password used for the tests")
)
// Simple test to fetch the base Redfish URL and assert a 200 OK response.
func TestRedfishV1Availability(t *testing.T) {
var (
url = fmt.Sprintf("%s/redfish/v1", *host)
body = []byte{}
headers = map[string]string{}
)
res, b, err := client.MakeRequest(nil, url, http.MethodGet, body, headers)
if err != nil {
t.Fatalf("failed to make request to BMC: %v", err)
}
func checkResponse(res *http.Response, b []byte) error {
// test for a 200 response code here
if res.StatusCode != http.StatusOK {
t.Fatalf("expected response code to return status code 200")
return fmt.Errorf("expected response code to return status code 200")
}
// make sure the response body is not empty
if len(b) <= 0 {
t.Fatalf("expected response body to not be empty")
return fmt.Errorf("expected response body to not be empty")
}
// make sure the response body is in a JSON format
if json.Valid(b) {
t.Fatalf("expected response body to be valid JSON")
// make sure the response body is in a valid JSON format
if !json.Valid(b) {
return fmt.Errorf("expected response body to be valid JSON")
}
return nil
}
// Simple test to fetch the base Redfish URL and assert a 200 OK response.
func TestRedfishV1ServiceRootAvailability(t *testing.T) {
var (
url = fmt.Sprintf("%s/redfish/v1/", *host)
body = []byte{}
headers = map[string]string{}
testClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
err error
)
// set up the emulator to run before test
err = waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
res, b, err := client.MakeRequest(testClient, url, http.MethodGet, body, headers)
if err != nil {
t.Fatalf("failed to make request to BMC node: %v", err)
}
err = checkResponse(res, b)
if err != nil {
t.Fatalf("failed to check response for redfish service root: %v", err)
}
}
// Simple test to ensure an expected Redfish version minimum requirement.
func TestRedfishVersion(t *testing.T) {
func TestRedfishV1Version(t *testing.T) {
var (
url string = fmt.Sprintf("%s/redfish/v1", *host)
body client.HTTPBody = []byte{}
headers client.HTTPHeader = map[string]string{}
err error
url string = fmt.Sprintf("%s/redfish/v1/", *host)
body client.HTTPBody = []byte{}
headers client.HTTPHeader = map[string]string{}
testClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
root map[string]any
err error
)
_, _, err = client.MakeRequest(nil, url, http.MethodGet, body, headers)
res, b, err := client.MakeRequest(testClient, url, http.MethodGet, body, headers)
if err != nil {
log.Error().Err(err).Msg("failed to make request")
t.Fatalf("failed to make request to BMC node: %v", err)
}
err = checkResponse(res, b)
if err != nil {
t.Fatalf("failed to check response for redfish version: %v", err)
}
// check the "RedfishVersion" from service root
err = json.Unmarshal(b, &root)
if err != nil {
t.Fatalf("failed to unmarshal redfish response: %v", err)
}
_, ok := root["RedfishVersion"]
if !ok {
t.Fatalf("failed to get 'RedfishVersion' from service root")
}
}
@ -72,13 +114,19 @@ func TestRedfishVersion(t *testing.T) {
// that we need for Magellan to run correctly. This test differs from the
// `TestCrawlCommand` testing function as it is not checking specifically
// for functionality.
func TestExpectedProperties(t *testing.T) {
func TestExpectedOutput(t *testing.T) {
// make sure what have a valid host
if host == nil {
t.Fatal("invalid host (host is nil)")
}
systems, err := crawler.CrawlBMC(
// set up the emulator to run before test
err := waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
systems, err := crawler.CrawlBMCForSystems(
crawler.CrawlerConfig{
URI: *host,
Username: *username,
@ -106,8 +154,5 @@ func TestExpectedProperties(t *testing.T) {
if len(system.EthernetInterfaces) <= 0 {
t.Errorf("no ethernet interfaces found for system '%s'", system.Name)
}
if len(system.NetworkInterfaces) <= 0 {
t.Errorf("no network interfaces found for system '%s'", system.Name)
}
}
}