Merge branch 'main' into config-file

This commit is contained in:
David J. Allen 2024-05-07 14:57:34 -06:00
commit 2b421e8af5
No known key found for this signature in database
GPG key ID: 717C593FF60A2ACC
24 changed files with 618 additions and 298 deletions

4
.devcontainer/.env Normal file
View file

@ -0,0 +1,4 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres
POSTGRES_HOSTNAME=localhost

13
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,13 @@
FROM mcr.microsoft.com/devcontainers/go:1-1.20-bullseye
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment the next lines to use go get to install anything else you need
# USER vscode
# RUN go get -x <your-dependency-or-tool>
# USER root
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

View file

@ -0,0 +1,28 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/go-postgres
{
"name": "Go & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
"ghcr.io/marcozac/devcontainer-features/goreleaser:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
}
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Configure tool-specific properties.
// "customizations": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [5432],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "go version",
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View file

@ -0,0 +1,38 @@
version: '3.8'
volumes:
postgres-data:
services:
app:
build:
context: .
dockerfile: Dockerfile
env_file:
# Ensure that the variables in .env match the same variables in devcontainer.json
- .env
volumes:
- ../..:/workspaces:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
env_file:
# Ensure that the variables in .env match the same variables in devcontainer.json
- .env
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)

View file

@ -3,25 +3,29 @@ before:
- go mod download
builds:
- env:
- CGO_ENABLED=0
- CGO_ENABLED=1
goos:
- darwin
- linux
- windows
goarch:
- amd64
- arm64
dockers:
- image_templates:
- bikeshack/{{.ProjectName}}:latest
- bikeshack/{{.ProjectName}}:{{ .Tag }}
- bikeshack/{{.ProjectName}}:{{ .Major }}
- bikeshack/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}
- ghcr.io/openchami/{{.ProjectName}}:latest
- ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}
- ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}
- ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
extra_files:
- LICENSE
- CHANGELOG.md
- README.md
- bin/magellan.sh
archives:
- format: tar.gz
rlcp: true
@ -34,10 +38,10 @@ archives:
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
files:
- migrations/*
- LICENSE
- CHANGELOG.md
- README.md
- bin/magellan.sh
checksum:
name_template: 'checksums.txt'
snapshot:

View file

@ -5,7 +5,18 @@ 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).
## [Unreleased]
## [0.0.5] - 2023-11-02
### Added
* Ability to update firmware
* Refactored connection handling for faster scanning
* Updated to refelct home at github.com/OpenCHAMI
* Updated to reflect ghcr.io as container home
## [Unreleased]
## [0.0.1] - 2023-09-14

View file

@ -1,13 +1,15 @@
FROM cgr.dev/chainguard/wolfi-base
RUN apk add --no-cache tini
RUN apk add --no-cache tini bash
# nobody 65534:65534
USER 65534:65534
COPY magellan /
COPY magellan /magellan
COPY /bin/magellan.sh /magellan.sh
CMD [ "/magellan" ]
CMD [ "/magellan.sh" ]
ENTRYPOINT [ "/sbin/tini", "--" ]

View file

@ -41,6 +41,9 @@ go mod tidy && go build
This should find and download all of the required dependencies. Although other
versions of Go may work, the project has only been tested with v1.20.
To build the Docker container, run `docker build -t magellan:latest .` in the
project's directory.
## Usage
There are three main commands to use with the tool: `scan`, `list`, and `collect`.
@ -81,6 +84,13 @@ to find the `hms-smd` API.
Note: If the `db.path` flag is not set, `magellan` will use /tmp/magellan.db by default.
Both the `scan` and `collect` commands can be ran via Docker after pulling the image:
```bash
docker pull bikeshack/magellan:latest
docker run bikeshack/magellan:latest /magellan.sh --scan "--subnet 172.16.0.0 --port 443 --timeout 3" --collect "--user admin --pass password --host http://vm01 --port 27779"
```
## TODO
List of things left to fix, do, or ideas...
@ -89,9 +99,9 @@ List of things left to fix, do, or ideas...
* [ ] Set default port automatically depending on the driver used to scan
* [X] Test using different `bmclib` supported drivers (mainly 'redfish')
* [X] Confirm loading different components into `hms-smd`
* [ ] Add ability to set subnet mask for scanning
* [X] Add ability to set subnet mask for scanning
* [ ] Add unit tests for `scan`, `list`, and `collect` commands
* [ ] Clean up, remove unused, and tidy code
* [X] Clean up, remove unused, and tidy code
## Copyright

121
bin/magellan.sh Normal file → Executable file
View file

@ -1,21 +1,126 @@
#!/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 build(){
go mod tidy && go build -C bin/magellan
}
function scan() {
./magellan scan --subnet 172.16.0.0 --port 443
# ./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
}
function update() {
./magellan update --user admin --pass password --host 172.16.0.109 --component BMC --protocol HTTP --firmware-path ""
function list(){
# ./magellan list
${EXE} list
}
function collect() {
./magellan collect --user admin --pass password
# ./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

@ -1,10 +1,10 @@
package cmd
import (
magellan "github.com/bikeshack/magellan/internal"
"github.com/bikeshack/magellan/internal/api/smd"
"github.com/bikeshack/magellan/internal/db/sqlite"
"github.com/bikeshack/magellan/internal/log"
magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/OpenCHAMI/magellan/internal/api/smd"
"github.com/OpenCHAMI/magellan/internal/db/sqlite"
"github.com/OpenCHAMI/magellan/internal/log"
"github.com/cznic/mathutil"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -28,21 +28,31 @@ var collectCmd = &cobra.Command{
l.Log.Errorf("could not get states: %v", err)
}
// try to load access token either from env var, file, or config if var not set
if accessToken == "" {
var err error
accessToken, err = LoadAccessToken()
if err != nil {
l.Log.Errorf("failed to load access token: %v", err)
}
}
//
if threads <= 0 {
threads = mathutil.Clamp(len(probeStates), 1, 255)
}
q := &magellan.QueryParams{
User: user,
Pass: pass,
Protocol: protocol,
Drivers: drivers,
Preferred: preferredDriver,
Timeout: timeout,
Threads: threads,
Verbose: verbose,
WithSecureTLS: withSecureTLS,
OutputPath: outputPath,
ForceUpdate: forceUpdate,
User: user,
Pass: pass,
Protocol: protocol,
Drivers: drivers,
Preferred: preferredDriver,
Timeout: timeout,
Threads: threads,
Verbose: verbose,
CaCertPath: cacertPath,
OutputPath: outputPath,
ForceUpdate: forceUpdate,
}
magellan.CollectAll(&probeStates, l, q)
@ -81,6 +91,5 @@ func init() {
viper.BindPFlag("collect.secure-tls", collectCmd.Flags().Lookup("secure-tls"))
viper.BindPFlag("collect.cert-pool", collectCmd.Flags().Lookup("cert-pool"))
rootCmd.AddCommand(collectCmd)
}

View file

@ -3,7 +3,7 @@ package cmd
import (
"fmt"
"github.com/bikeshack/magellan/internal/db/sqlite"
"github.com/OpenCHAMI/magellan/internal/db/sqlite"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"

84
cmd/login.go Normal file
View file

@ -0,0 +1,84 @@
package cmd
import (
"errors"
"fmt"
"net/http"
"os"
magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/OpenCHAMI/magellan/internal/log"
"github.com/lestrrat-go/jwx/jwt"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
loginUrl string
targetHost string
targetPort int
tokenPath string
forceLogin bool
noBrowser bool
)
var loginCmd = &cobra.Command{
Use: "login",
Short: "Log in with identity provider for access token",
Long: "",
Run: func(cmd *cobra.Command, args []string) {
// make application logger
l := log.NewLogger(logrus.New(), logrus.DebugLevel)
// check if we have a valid JWT before starting login
if !forceLogin {
// try getting the access token from env var
testToken, err := LoadAccessToken()
if err != nil {
l.Log.Errorf("failed to load access token: %v", err)
}
// parse into jwt.Token to validate
token, err := jwt.Parse([]byte(testToken))
if err != nil {
fmt.Printf("failed to parse access token contents: %v\n", err)
return
}
// check if the token is invalid and we need a new one
err = jwt.Validate(token)
if err != nil {
fmt.Printf("failed to validate access token...fetching a new one")
} else {
fmt.Printf("found a valid token...skipping login (use the '-f/--force' flag to login anyway)")
return
}
}
// start the login flow
var err error
accessToken, err = magellan.Login(loginUrl, targetHost, targetPort)
if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n")
} else if err != nil {
fmt.Printf("failed to start server: %v\n", err)
}
// if we got a new token successfully, save it to the token path
if accessToken != "" && tokenPath != "" {
err := os.WriteFile(tokenPath, []byte(accessToken), os.ModePerm)
if err != nil {
fmt.Printf("failed to write access token to file: %v\n", err)
}
}
},
}
func init() {
loginCmd.Flags().StringVar(&loginUrl, "url", "http://127.0.0.1:3333/login", "set the login URL")
loginCmd.Flags().StringVar(&targetHost, "target-host", "127.0.0.1", "set the target host to return the access code")
loginCmd.Flags().IntVar(&targetPort, "target-port", 5000, "set the target host to return the access code")
loginCmd.Flags().BoolVarP(&forceLogin, "force", "f", false, "start the login process even with a valid token")
loginCmd.Flags().StringVar(&tokenPath, "token-path", ".ochami-token", "set the path the load/save the access token")
loginCmd.Flags().BoolVar(&noBrowser, "no-browser", false, "prevent the default browser from being opened automatically")
rootCmd.AddCommand(loginCmd)
}

View file

@ -12,21 +12,21 @@ import (
)
var (
accessToken string
timeout int
threads int
ports []int
hosts []string
protocol string
withSecureTLS bool
certPoolFile string
cacertPath string
user string
pass string
dbpath string
drivers []string
preferredDriver string
ipmitoolPath string
outputPath string
configPath string
outputPath string
configPath string
verbose bool
)
@ -54,12 +54,30 @@ func Execute() {
}
}
func LoadAccessToken() (string, error) {
// try to load token from env var
testToken := os.Getenv("OCHAMI_ACCESS_TOKEN")
if testToken != "" {
return testToken, nil
}
// try reading access token from a file
b, err := os.ReadFile(tokenPath)
if err == nil {
return string(b), nil
}
// TODO: try to load token from config
return "", fmt.Errorf("could not load from environment variable or file")
}
func init() {
cobra.OnInitialize(InitializeConfig)
rootCmd.PersistentFlags().IntVar(&threads, "threads", -1, "set the number of threads")
rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 30, "set the timeout")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config file path")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", true, "set verbose flag")
rootCmd.PersistentFlags().StringVar(&accessToken, "access-token", "", "set the access token")
rootCmd.PersistentFlags().StringVar(&dbpath, "db.path", "/tmp/magellan/magellan.db", "set the probe storage path")
// bind viper config flags with cobra

View file

@ -6,8 +6,8 @@ import (
"os"
"path"
magellan "github.com/bikeshack/magellan/internal"
"github.com/bikeshack/magellan/internal/db/sqlite"
magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/OpenCHAMI/magellan/internal/db/sqlite"
"github.com/cznic/mathutil"
"github.com/spf13/cobra"
@ -15,10 +15,10 @@ import (
)
var (
begin uint8
end uint8
subnets []string
subnetMasks []net.IP
begin uint8
end uint8
subnets []string
subnetMasks []net.IP
disableProbing bool
)
@ -37,7 +37,7 @@ var scanCmd = &cobra.Command{
return
}
if len(subnetMasks) < i + 1 {
if len(subnetMasks) < i+1 {
subnetMasks = append(subnetMasks, net.IP{255, 255, 255, 0})
}

View file

@ -1,43 +1,42 @@
package cmd
import (
magellan "github.com/bikeshack/magellan/internal"
"github.com/bikeshack/magellan/internal/log"
magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/OpenCHAMI/magellan/internal/log"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
host string
port int
firmwareUrl string
firmwareVersion string
component string
host string
port int
firmwareUrl string
firmwareVersion string
component string
transferProtocol string
status bool
status bool
)
var updateCmd = &cobra.Command{
Use: "update",
Use: "update",
Short: "Update BMC node firmware",
Run: func(cmd *cobra.Command, args []string) {
l := log.NewLogger(logrus.New(), logrus.DebugLevel)
q := &magellan.UpdateParams {
FirmwarePath: firmwareUrl,
FirmwareVersion: firmwareVersion,
Component: component,
q := &magellan.UpdateParams{
FirmwarePath: firmwareUrl,
FirmwareVersion: firmwareVersion,
Component: component,
TransferProtocol: transferProtocol,
QueryParams: magellan.QueryParams{
Drivers: []string{"redfish"},
Drivers: []string{"redfish"},
Preferred: "redfish",
Protocol: protocol,
Host: host,
User: user,
Pass: pass,
Timeout: timeout,
Port: port,
WithSecureTLS: withSecureTLS,
Protocol: protocol,
Host: host,
User: user,
Pass: pass,
Timeout: timeout,
Port: port,
},
}
@ -77,7 +76,6 @@ func init() {
updateCmd.Flags().StringVar(&firmwareUrl, "firmware-url", "", "set the path to the firmware")
updateCmd.Flags().StringVar(&firmwareVersion, "firmware-version", "", "set the version of firmware to be installed")
updateCmd.Flags().StringVar(&component, "component", "", "set the component to upgrade")
updateCmd.Flags().BoolVar(&withSecureTLS, "secure-tls", false, "enable secure TLS")
updateCmd.Flags().BoolVar(&status, "status", false, "get the status of the update")
viper.BindPFlag("bmc-host", updateCmd.Flags().Lookup("bmc-host"))

12
go.mod
View file

@ -1,4 +1,4 @@
module github.com/bikeshack/magellan
module github.com/OpenCHAMI/magellan
go 1.20
@ -6,9 +6,12 @@ require (
github.com/Cray-HPE/hms-xname v1.3.0
github.com/bmc-toolbox/bmclib/v2 v2.0.1-0.20230714152943-a1b87e2ff47f
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548
github.com/go-chi/chi/v5 v5.0.12
github.com/jacobweinstock/registrar v0.4.7
github.com/jmoiron/sqlx v1.3.5
github.com/lestrrat-go/jwx v1.2.29
github.com/mattn/go-sqlite3 v1.14.6
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.17.0
@ -22,6 +25,7 @@ require (
github.com/bmc-toolbox/common v0.0.0-20230717121556-5eb9915a8a5a // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
@ -30,6 +34,12 @@ require (
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect

View file

@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/bikeshack/magellan/internal/util"
"github.com/OpenCHAMI/magellan/internal/util"
"github.com/jmoiron/sqlx"
)

View file

@ -5,9 +5,8 @@ package smd
// https://github.com/alexlovelltroy/hms-smd
import (
"fmt"
"net/http"
"github.com/bikeshack/magellan/internal/util"
"github.com/OpenCHAMI/magellan/internal/util"
// hms "github.com/alexlovelltroy/hms-smd"
)
@ -52,8 +51,9 @@ func AddRedfishEndpoint(data []byte, headers map[string]string) error {
url := makeEndpointUrl("/Inventory/RedfishEndpoints")
res, body, err := util.MakeRequest(url, "POST", data, headers)
if res != nil {
if res.StatusCode != http.StatusOK {
return fmt.Errorf("could not add redfish endpoint")
statusOk := res.StatusCode >= 200 && res.StatusCode < 300
if !statusOk {
return fmt.Errorf("returned status code %d when adding endpoint", res.StatusCode)
}
fmt.Printf("%v (%v)\n%s\n", url, res.Status, string(body))
}
@ -69,7 +69,8 @@ func UpdateRedfishEndpoint(xname string, data []byte, headers map[string]string)
res, body, err := util.MakeRequest(url, "PUT", data, headers)
fmt.Printf("%v (%v)\n%s\n", url, res.Status, string(body))
if res != nil {
if res.StatusCode != http.StatusOK {
statusOk := res.StatusCode >= 200 && res.StatusCode < 300
if !statusOk {
return fmt.Errorf("could not update redfish endpoint")
}
}

View file

@ -12,10 +12,10 @@ import (
"sync"
"time"
"github.com/bikeshack/magellan/internal/log"
"github.com/OpenCHAMI/magellan/internal/log"
"github.com/bikeshack/magellan/internal/api/smd"
"github.com/bikeshack/magellan/internal/util"
"github.com/OpenCHAMI/magellan/internal/api/smd"
"github.com/OpenCHAMI/magellan/internal/util"
"github.com/Cray-HPE/hms-xname/xnames"
bmclib "github.com/bmc-toolbox/bmclib/v2"
@ -33,27 +33,27 @@ const (
HTTPS_PORT = 443
)
// NOTE: ...params were getting too long...
type QueryParams struct {
Host string
Port int
Protocol string
User string
Pass string
Drivers []string
Threads int
Preferred string
Timeout int
WithSecureTLS bool
CertPoolFile string
Verbose bool
IpmitoolPath string
OutputPath string
ForceUpdate bool
Host string
Port int
Protocol string
User string
Pass string
Drivers []string
Threads int
Preferred string
Timeout int
CaCertPath string
Verbose bool
IpmitoolPath string
OutputPath string
ForceUpdate bool
AccessToken string
}
func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
@ -75,9 +75,9 @@ func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) {
}
// only work if valid cert is provided
if q.WithSecureTLS && q.CertPoolFile != "" {
if q.CaCertPath != "" {
pool := x509.NewCertPool()
data, err := os.ReadFile(q.CertPoolFile)
data, err := os.ReadFile(q.CaCertPath)
if err != nil {
return nil, fmt.Errorf("could not read cert pool file: %v", err)
}
@ -122,15 +122,8 @@ func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) err
done := make(chan struct{}, q.Threads+1)
chanProbeState := make(chan ScannedResult, q.Threads+1)
// generate custom xnames for bmcs
node := xnames.Node{
Cabinet: 1000,
Chassis: 1,
ComputeModule: 7,
NodeBMC: -1,
}
// collect bmc information asynchronously
var offset = 0
var wg sync.WaitGroup
wg.Add(q.Threads)
for i := 0; i < q.Threads; i++ {
@ -144,23 +137,34 @@ func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) err
q.Host = ps.Host
q.Port = ps.Port
client, err := NewClient(l, q)
if err != nil {
l.Log.Errorf("could not make client: %v", err)
continue
// generate custom xnames for bmcs
node := xnames.Node{
Cabinet: 1000,
Chassis: 1,
ComputeModule: 7,
NodeBMC: offset,
}
offset += 1
node.NodeBMC += 1
// bmclibClient, err := NewClient(l, q)
// if err != nil {
// l.Log.Errorf("could not make client: %v", err)
// }
gofishClient, err := connectGofish(q)
if err != nil {
l.Log.Errorf("could not connect to bmc (%v:%v): %v", q.Host, q.Port, err)
}
// data to be sent to smd
data := map[string]any{
"ID": fmt.Sprintf("%v", node.String()[:len(node.String())-2]),
"Type": "",
"Name": "",
"FQDN": ps.Host,
"User": q.User,
"Password": q.Pass,
"MACRequired": true,
"ID": fmt.Sprintf("%v", node.String()[:len(node.String())-2]),
"Type": "",
"Name": "",
"FQDN": ps.Host,
"User": q.User,
"Password": q.Pass,
"MACRequired": true,
"RediscoverOnUpdate": false,
}
@ -168,96 +172,72 @@ func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) err
var rm map[string]json.RawMessage
// inventories
inventory, err := CollectInventory(client, q)
if err != nil {
l.Log.Errorf("could not query inventory (%v:%v): %v", q.Host, q.Port, err)
}
json.Unmarshal(inventory, &rm)
data["Inventory"] = rm["Inventory"]
// if bmclibClient != nil {
// inventory, err := CollectInventory(bmclibClient, q)
// if err != nil {
// l.Log.Errorf("could not query inventory (%v:%v): %v", q.Host, q.Port, err)
// }
// json.Unmarshal(inventory, &rm)
// data["Inventory"] = rm["Inventory"]
// }
// chassis
chassis, err := CollectChassis(q)
if err != nil {
l.Log.Errorf("could not query chassis: %v", err)
continue
if gofishClient != nil {
chassis, err := CollectChassis(gofishClient, q)
if err != nil {
l.Log.Errorf("could not query chassis: %v", err)
continue
}
json.Unmarshal(chassis, &rm)
data["Chassis"] = rm["Chassis"]
// systems
systems, err := CollectSystems(gofishClient, q)
if err != nil {
l.Log.Errorf("could not query systems: %v", err)
}
json.Unmarshal(systems, &rm)
data["Systems"] = rm["Systems"]
// add other fields from systems
if len(rm["Systems"]) > 0 {
var s map[string][]interface{}
json.Unmarshal(rm["Systems"], &s)
data["Name"] = s["Name"]
}
}
json.Unmarshal(chassis, &rm)
data["Chassis"] = rm["Chassis"]
// ethernet interfaces
// interfaces, err := QueryEthernetInterfaces(client, q)
// if err != nil {
// l.Log.Errorf("could not query ethernet interfaces: %v", err)
// continue
// }
// json.Unmarshal(interfaces, &rm)
// data["Interfaces"] = rm["Interfaces"]
// storage
// storage, err := QueryStorage(q)
// if err != nil {
// l.Log.Errorf("could not query storage: %v", err)
// continue
// }
// json.Unmarshal(storage, &rm)
// data["Storage"] = rm["Storage"]
// get specific processor info
// procs, err := QueryProcessors(q)
// if err != nil {
// l.Log.Errorf("could not query processors: %v", err)
// }
// var p map[string]interface{}
// json.Unmarshal(procs, &p)
// data["Processors"] = rm["Processors"]
// systems
systems, err := CollectSystems(client, q)
if err != nil {
l.Log.Errorf("could not query systems: %v", err)
}
json.Unmarshal(systems, &rm)
data["Systems"] = rm["Systems"]
// add other fields from systems
if len(rm["Systems"]) > 0 {
var s map[string][]interface{}
json.Unmarshal(rm["Systems"], &s)
data["Name"] = s["Name"]
}
// data["Type"] = rm[""]
// registries
// registries, err := QueryRegisteries(q)
// if err != nil {
// l.Log.Errorf("could not query registries: %v", err)
// }
// json.Unmarshal(registries, &rm)
// data["Registries"] = rm["Registries"]
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
b, err := json.MarshalIndent(data, "", " ")
// use access token in authorization header if we have it
if q.AccessToken != "" {
headers["Authorization"] = "Bearer " + q.AccessToken
}
body, err := json.MarshalIndent(data, "", " ")
if err != nil {
l.Log.Errorf("could not marshal JSON: %v", err)
}
if q.Verbose {
fmt.Printf("%v\n", string(body))
}
// write JSON data to file
err = os.WriteFile(path.Clean(outputPath + "/" + q.Host + ".json"), b, os.ModePerm)
err = os.WriteFile(path.Clean(outputPath+"/"+q.Host+".json"), body, os.ModePerm)
if err != nil {
l.Log.Errorf("could not write data to file: %v", err)
}
// add all endpoints to smd
err = smd.AddRedfishEndpoint(b, headers)
err = smd.AddRedfishEndpoint(body, headers)
if err != nil {
l.Log.Error(err)
// try updating instead
if q.ForceUpdate {
err = smd.UpdateRedfishEndpoint(data["ID"].(string), b, headers)
err = smd.UpdateRedfishEndpoint(data["ID"].(string), body, headers)
if err != nil {
l.Log.Error(err)
}
@ -324,9 +304,6 @@ func CollectMetadata(client *bmclib.Client, q *QueryParams) ([]byte, error) {
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
if q.Verbose {
fmt.Printf("Metadata: %v\n", string(b))
}
ctxCancel()
return b, nil
}
@ -340,7 +317,6 @@ func CollectInventory(client *bmclib.Client, q *QueryParams) ([]byte, error) {
ctxCancel()
return nil, fmt.Errorf("could not open client: %v", err)
}
defer client.Close(ctx)
inventory, err := client.Inventory(ctx)
if err != nil {
@ -348,7 +324,6 @@ func CollectInventory(client *bmclib.Client, q *QueryParams) ([]byte, error) {
return nil, fmt.Errorf("could not get inventory: %v", err)
}
// retrieve inventory data
data := map[string]any{"Inventory": inventory}
b, err := json.MarshalIndent(data, "", " ")
@ -357,9 +332,6 @@ func CollectInventory(client *bmclib.Client, q *QueryParams) ([]byte, error) {
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
ctxCancel()
return b, nil
}
@ -372,7 +344,6 @@ func CollectPowerState(client *bmclib.Client, q *QueryParams) ([]byte, error) {
ctxCancel()
return nil, fmt.Errorf("could not open client: %v", err)
}
defer client.Close(ctx)
powerState, err := client.GetPowerState(ctx)
if err != nil {
@ -388,9 +359,6 @@ func CollectPowerState(client *bmclib.Client, q *QueryParams) ([]byte, error) {
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
ctxCancel()
return b, nil
@ -415,18 +383,14 @@ func CollectUsers(client *bmclib.Client, q *QueryParams) ([]byte, error) {
}
// retrieve inventory data
data := map[string]any {"Users": users}
data := map[string]any{"Users": users}
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
ctxCancel()
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
// return b, nil
ctxCancel()
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil
}
@ -436,18 +400,10 @@ func CollectBios(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// return nil, fmt.Errorf("could not make query: %v", err)
// }
b, err := makeRequest(client, client.GetBiosConfiguration, q.Timeout)
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, err
}
func CollectEthernetInterfaces(client *bmclib.Client, q *QueryParams, systemID string) ([]byte, error) {
c, err := connectGofish(q)
if err != nil {
return nil, fmt.Errorf("could not connect to bmc: %v", err)
}
func CollectEthernetInterfaces(c *gofish.APIClient, q *QueryParams, systemID string) ([]byte, error) {
systems, err := c.Service.Systems()
if err != nil {
return nil, fmt.Errorf("could not query storage systems (%v:%v): %v", q.Host, q.Port, err)
@ -455,7 +411,7 @@ func CollectEthernetInterfaces(client *bmclib.Client, q *QueryParams, systemID s
var interfaces []*redfish.EthernetInterface
for _, system := range systems {
i, err := redfish.ListReferencedEthernetInterfaces(c, "/redfish/v1/Systems/" + system.ID + "/EthernetInterfaces/")
i, err := redfish.ListReferencedEthernetInterfaces(c, "/redfish/v1/Systems/"+system.ID+"/EthernetInterfaces/")
if err != nil {
continue
}
@ -472,17 +428,10 @@ func CollectEthernetInterfaces(client *bmclib.Client, q *QueryParams, systemID s
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
// if q.Verbose {
// fmt.Printf("%v\n", string(b))
// }
return b, nil
}
func CollectChassis(q *QueryParams) ([]byte, error) {
c, err := connectGofish(q)
if err != nil {
return nil, fmt.Errorf("could not connect to bmc (%v:%v): %v", q.Host, q.Port, err)
}
func CollectChassis(c *gofish.APIClient, q *QueryParams) ([]byte, error) {
chassis, err := c.Service.Chassis()
if err != nil {
return nil, fmt.Errorf("could not query chassis (%v:%v): %v", q.Host, q.Port, err)
@ -494,18 +443,10 @@ func CollectChassis(q *QueryParams) ([]byte, error) {
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil
}
func CollectStorage(q *QueryParams) ([]byte, error) {
c, err := connectGofish(q)
if err != nil {
return nil, fmt.Errorf("could not connect to bmc (%v:%v): %v", q.Host, q.Port, err)
}
func CollectStorage(c *gofish.APIClient, q *QueryParams) ([]byte, error) {
systems, err := c.Service.StorageSystems()
if err != nil {
return nil, fmt.Errorf("could not query storage systems (%v:%v): %v", q.Host, q.Port, err)
@ -518,7 +459,7 @@ func CollectStorage(q *QueryParams) ([]byte, error) {
data := map[string]any{
"Storage": map[string]any{
"Systems": systems,
"Systems": systems,
"Services": services,
},
}
@ -527,18 +468,10 @@ func CollectStorage(q *QueryParams) ([]byte, error) {
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil
}
func CollectSystems(client *bmclib.Client, q *QueryParams) ([]byte, error) {
c, err := connectGofish(q)
if err != nil {
return nil, fmt.Errorf("could not connect to bmc (%v:%v): %v", q.Host, q.Port, err)
}
func CollectSystems(c *gofish.APIClient, q *QueryParams) ([]byte, error) {
systems, err := c.Service.Systems()
if err != nil {
return nil, fmt.Errorf("could not query systems (%v:%v): %v", q.Host, q.Port, err)
@ -547,50 +480,39 @@ func CollectSystems(client *bmclib.Client, q *QueryParams) ([]byte, error) {
// query the system's ethernet interfaces
var temp []map[string]any
for _, system := range systems {
interfaces, err := CollectEthernetInterfaces(client, q, system.ID)
interfaces, err := CollectEthernetInterfaces(c, q, system.ID)
if err != nil {
continue
}
var i map[string]any
json.Unmarshal(interfaces, &i)
temp = append(temp, map[string]any{
"Data": system,
"Data": system,
"EthernetInterfaces": i["EthernetInterfaces"],
})
}
data := map[string]any{"Systems": temp }
data := map[string]any{"Systems": temp}
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil
}
func CollectRegisteries(q *QueryParams) ([]byte, error) {
c, err := connectGofish(q)
if err != nil {
return nil, fmt.Errorf("could not connect to bmc (%v:%v): %v", q.Host, q.Port, err)
}
func CollectRegisteries(c *gofish.APIClient, q *QueryParams) ([]byte, error) {
registries, err := c.Service.Registries()
if err != nil {
return nil, fmt.Errorf("could not query storage systems (%v:%v): %v", q.Host, q.Port, err)
}
data := map[string]any{"Registries": registries }
data := map[string]any{"Registries": registries}
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil
}
@ -625,45 +547,71 @@ func CollectProcessors(q *QueryParams) ([]byte, error) {
}
}
data := map[string]any{"Processors": procs }
data := map[string]any{"Processors": procs}
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return nil, fmt.Errorf("could not marshal JSON: %v", err)
}
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil
}
func connectGofish(q *QueryParams) (*gofish.APIClient, error) {
config := makeGofishConfig(q)
config, err := makeGofishConfig(q)
if err != nil {
return nil, fmt.Errorf("failed to make gofish config: %v", err)
}
c, err := gofish.Connect(config)
if err != nil {
return nil, fmt.Errorf("could not connect to redfish endpoint: %v", err)
}
if c != nil {
c.Service.ProtocolFeaturesSupported = gofish.ProtocolFeaturesSupported{
ExpandQuery: gofish.Expand{
ExpandAll: true,
Links: true,
Links: true,
},
}
}
return c, err
}
func makeGofishConfig(q *QueryParams) gofish.ClientConfig {
url := baseRedfishUrl(q)
func makeGofishConfig(q *QueryParams) (gofish.ClientConfig, error) {
var (
client = &http.Client{}
url = baseRedfishUrl(q)
config = gofish.ClientConfig{
Endpoint: url,
Username: q.User,
Password: q.Pass,
Insecure: q.CaCertPath == "",
TLSHandshakeTimeout: q.Timeout,
HTTPClient: client,
// MaxConcurrentRequests: int64(q.Threads), // NOTE: this was added in latest gofish
}
)
if q.CaCertPath != "" {
cacert, err := os.ReadFile(q.CaCertPath)
if err != nil {
return config, fmt.Errorf("failed to read CA cert file: %v", err)
}
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(cacert)
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
}
}
return gofish.ClientConfig{
Endpoint: url,
Username: q.User,
Password: q.Pass,
Insecure: !q.WithSecureTLS,
Insecure: q.CaCertPath == "",
TLSHandshakeTimeout: q.Timeout,
}
HTTPClient: client,
// MaxConcurrentRequests: int64(q.Threads), // NOTE: this was added in latest gofish
}, nil
}
func makeRequest[T any](client *bmclib.Client, fn func(context.Context) (T, error), timeout int) ([]byte, error) {

View file

@ -3,7 +3,7 @@ package sqlite
import (
"fmt"
magellan "github.com/bikeshack/magellan/internal"
magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/jmoiron/sqlx"
)

41
internal/login.go Normal file
View file

@ -0,0 +1,41 @@
package magellan
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/pkg/browser"
)
func Login(loginUrl string, targetHost string, targetPort int) (string, error) {
var accessToken string
// check and make sure the login URL isn't empty
if loginUrl == "" {
return "", fmt.Errorf("no login URL provided")
}
// if a target host and port are provided, then add to URL
if targetHost != "" && targetPort > 0 && targetPort < 65536 {
loginUrl += fmt.Sprintf("?target=http://%s:%d", targetHost, targetPort)
}
// open browser with the specified URL
err := browser.OpenURL(loginUrl)
if err != nil {
return "", fmt.Errorf("failed to open browser: %v", err)
}
// start a temporary server to listen for token
s := http.Server{
Addr: fmt.Sprintf("%s:%d", targetHost, targetPort),
}
r := chi.NewRouter()
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// try and extract access token from headers
accessToken = r.Header.Get("access_token")
s.Close()
})
return accessToken, s.ListenAndServe()
}

View file

@ -8,7 +8,7 @@ import (
"sync"
"time"
"github.com/bikeshack/magellan/internal/util"
"github.com/OpenCHAMI/magellan/internal/util"
)
type ScannedResult struct {
@ -51,7 +51,6 @@ func rawConnect(host string, ports []int, timeout int, keepOpenOnly bool) []Scan
return results
}
func GenerateHosts(subnet string, subnetMask *net.IP) []string {
if subnet == "" || subnetMask == nil {
return nil
@ -82,7 +81,7 @@ func generateHosts(ip *net.IP, mask *net.IPMask) []string {
// get all IP addresses in network
ones, _ := mask.Size()
hosts := []string{}
end := int(math.Pow(2, float64((32-ones))))-1
end := int(math.Pow(2, float64((32-ones)))) - 1
for i := 0; i < end; i++ {
// ip[3] = byte(i)
ip = util.GetNextIP(ip, 1)

View file

@ -10,20 +10,19 @@ import (
"strings"
"time"
"github.com/bikeshack/magellan/internal/log"
"github.com/bikeshack/magellan/internal/util"
"github.com/OpenCHAMI/magellan/internal/log"
"github.com/OpenCHAMI/magellan/internal/util"
bmclib "github.com/bmc-toolbox/bmclib/v2"
"github.com/bmc-toolbox/bmclib/v2/constants"
bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors"
"github.com/sirupsen/logrus"
)
type UpdateParams struct {
QueryParams
FirmwarePath string
FirmwareVersion string
Component string
FirmwarePath string
FirmwareVersion string
Component string
TransferProtocol string
}
@ -127,14 +126,14 @@ func UpdateFirmware(client *bmclib.Client, l *log.Logger, q *UpdateParams) error
func UpdateFirmwareRemote(q *UpdateParams) error {
url := baseRedfishUrl(&q.QueryParams) + "/redfish/v1/UpdateService/Actions/SimpleUpdate"
headers := map[string]string {
"Content-Type": "application/json",
headers := map[string]string{
"Content-Type": "application/json",
"cache-control": "no-cache",
}
b := map[string]any{
"UpdateComponent": q.Component, // BMC, BIOS
"UpdateComponent": q.Component, // BMC, BIOS
"TransferProtocol": q.TransferProtocol,
"ImageURI": q.FirmwarePath,
"ImageURI": q.FirmwarePath,
}
data, err := json.Marshal(b)
if err != nil {
@ -184,8 +183,6 @@ func GetUpdateStatus(q *UpdateParams) error {
// return fmt.Errorf("could not read file: %v", err)
// }
// switch q.TransferProtocol {
// case "HTTP":
// default:

View file

@ -1,7 +1,7 @@
package main
import (
"github.com/bikeshack/magellan/cmd"
"github.com/OpenCHAMI/magellan/cmd"
)
func main() {