mirror of
https://github.com/davidallendj/magellan.git
synced 2025-12-20 11:37:01 -07:00
Merge branch 'main' into cache-cmd
This commit is contained in:
commit
170df80621
23 changed files with 1118 additions and 356 deletions
31
.github/workflows/main.yml
vendored
31
.github/workflows/main.yml
vendored
|
|
@ -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
44
.github/workflows/prbuild.yml
vendored
Normal 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
3
.gitignore
vendored
|
|
@ -4,3 +4,6 @@ emulator/rf-emulator
|
|||
**.tar.gz
|
||||
**.tar.zst
|
||||
**.part
|
||||
dist/*
|
||||
**coverage.out**
|
||||
magellan.1
|
||||
114
.goreleaser.yaml
114
.goreleaser.yaml
|
|
@ -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
|
||||
|
|
|
|||
258
CHANGELOG.md
258
CHANGELOG.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
10
Dockerfile
10
Dockerfile
|
|
@ -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" ]
|
||||
|
|
|
|||
11
Makefile
11
Makefile
|
|
@ -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
|
||||
|
|
|
|||
35
README.md
35
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
122
bin/magellan.sh
122
bin/magellan.sh
|
|
@ -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"
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
14
cmd/root.go
14
cmd/root.go
|
|
@ -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")))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
29
dist/archlinux/PKGBUILD
vendored
29
dist/archlinux/PKGBUILD
vendored
|
|
@ -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
1
go.mod
|
|
@ -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 (
|
||||
|
|
|
|||
4
internal/cache/sqlite/sqlite.go
vendored
4
internal/cache/sqlite/sqlite.go
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
27
internal/util/util.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
60
internal/version/version.go
Normal file
60
internal/version/version.go
Normal 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)
|
||||
}
|
||||
7
main.go
7
main.go
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue