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

View file

@ -1,13 +1,15 @@
FROM cgr.dev/chainguard/wolfi-base FROM cgr.dev/chainguard/wolfi-base
RUN apk add --no-cache tini RUN apk add --no-cache tini bash
# nobody 65534:65534 # nobody 65534:65534
USER 65534:65534 USER 65534:65534
COPY magellan / COPY magellan /magellan
COPY /bin/magellan.sh /magellan.sh
CMD [ "/magellan" ]
CMD [ "/magellan.sh" ]
ENTRYPOINT [ "/sbin/tini", "--" ] 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 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. 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 ## Usage
There are three main commands to use with the tool: `scan`, `list`, and `collect`. 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. 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 ## TODO
List of things left to fix, do, or ideas... 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 * [ ] Set default port automatically depending on the driver used to scan
* [X] Test using different `bmclib` supported drivers (mainly 'redfish') * [X] Test using different `bmclib` supported drivers (mainly 'redfish')
* [X] Confirm loading different components into `hms-smd` * [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 * [ ] 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 ## 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(){ function build(){
go mod tidy && go build -C bin/magellan go mod tidy && go build -C bin/magellan
} }
function scan() { 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() { function list(){
./magellan list # ./magellan list
} ${EXE} list
function update() {
./magellan update --user admin --pass password --host 172.16.0.109 --component BMC --protocol HTTP --firmware-path ""
} }
function collect() { 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 package cmd
import ( import (
magellan "github.com/bikeshack/magellan/internal" magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/bikeshack/magellan/internal/api/smd" "github.com/OpenCHAMI/magellan/internal/api/smd"
"github.com/bikeshack/magellan/internal/db/sqlite" "github.com/OpenCHAMI/magellan/internal/db/sqlite"
"github.com/bikeshack/magellan/internal/log" "github.com/OpenCHAMI/magellan/internal/log"
"github.com/cznic/mathutil" "github.com/cznic/mathutil"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -28,21 +28,31 @@ var collectCmd = &cobra.Command{
l.Log.Errorf("could not get states: %v", err) 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 { if threads <= 0 {
threads = mathutil.Clamp(len(probeStates), 1, 255) threads = mathutil.Clamp(len(probeStates), 1, 255)
} }
q := &magellan.QueryParams{ q := &magellan.QueryParams{
User: user, User: user,
Pass: pass, Pass: pass,
Protocol: protocol, Protocol: protocol,
Drivers: drivers, Drivers: drivers,
Preferred: preferredDriver, Preferred: preferredDriver,
Timeout: timeout, Timeout: timeout,
Threads: threads, Threads: threads,
Verbose: verbose, Verbose: verbose,
WithSecureTLS: withSecureTLS, CaCertPath: cacertPath,
OutputPath: outputPath, OutputPath: outputPath,
ForceUpdate: forceUpdate, ForceUpdate: forceUpdate,
} }
magellan.CollectAll(&probeStates, l, q) magellan.CollectAll(&probeStates, l, q)
@ -81,6 +91,5 @@ func init() {
viper.BindPFlag("collect.secure-tls", collectCmd.Flags().Lookup("secure-tls")) viper.BindPFlag("collect.secure-tls", collectCmd.Flags().Lookup("secure-tls"))
viper.BindPFlag("collect.cert-pool", collectCmd.Flags().Lookup("cert-pool")) viper.BindPFlag("collect.cert-pool", collectCmd.Flags().Lookup("cert-pool"))
rootCmd.AddCommand(collectCmd) rootCmd.AddCommand(collectCmd)
} }

View file

@ -3,7 +3,7 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/bikeshack/magellan/internal/db/sqlite" "github.com/OpenCHAMI/magellan/internal/db/sqlite"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "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 ( var (
accessToken string
timeout int timeout int
threads int threads int
ports []int ports []int
hosts []string hosts []string
protocol string protocol string
withSecureTLS bool cacertPath string
certPoolFile string
user string user string
pass string pass string
dbpath string dbpath string
drivers []string drivers []string
preferredDriver string preferredDriver string
ipmitoolPath string ipmitoolPath string
outputPath string outputPath string
configPath string configPath string
verbose bool verbose bool
) )
@ -54,14 +54,32 @@ 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() { func init() {
cobra.OnInitialize(InitializeConfig) cobra.OnInitialize(InitializeConfig)
rootCmd.PersistentFlags().IntVar(&threads, "threads", -1, "set the number of threads") rootCmd.PersistentFlags().IntVar(&threads, "threads", -1, "set the number of threads")
rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 30, "set the timeout") rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 30, "set the timeout")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config file path") rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config file path")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", true, "set verbose flag") 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") rootCmd.PersistentFlags().StringVar(&dbpath, "db.path", "/tmp/magellan/magellan.db", "set the probe storage path")
// bind viper config flags with cobra // bind viper config flags with cobra
viper.BindPFlag("threads", rootCmd.Flags().Lookup("threads")) viper.BindPFlag("threads", rootCmd.Flags().Lookup("threads"))
viper.BindPFlag("timeout", rootCmd.Flags().Lookup("timeout")) viper.BindPFlag("timeout", rootCmd.Flags().Lookup("timeout"))
@ -112,4 +130,4 @@ func SetDefaults() {
viper.SetDefault("secure-tls", false) viper.SetDefault("secure-tls", false)
viper.SetDefault("status", false) viper.SetDefault("status", false)
} }

View file

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

View file

@ -1,46 +1,45 @@
package cmd package cmd
import ( import (
magellan "github.com/bikeshack/magellan/internal" magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/bikeshack/magellan/internal/log" "github.com/OpenCHAMI/magellan/internal/log"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var ( var (
host string host string
port int port int
firmwareUrl string firmwareUrl string
firmwareVersion string firmwareVersion string
component string component string
transferProtocol string transferProtocol string
status bool status bool
) )
var updateCmd = &cobra.Command{ var updateCmd = &cobra.Command{
Use: "update", Use: "update",
Short: "Update BMC node firmware", Short: "Update BMC node firmware",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
l := log.NewLogger(logrus.New(), logrus.DebugLevel) l := log.NewLogger(logrus.New(), logrus.DebugLevel)
q := &magellan.UpdateParams { q := &magellan.UpdateParams{
FirmwarePath: firmwareUrl, FirmwarePath: firmwareUrl,
FirmwareVersion: firmwareVersion, FirmwareVersion: firmwareVersion,
Component: component, Component: component,
TransferProtocol: transferProtocol, TransferProtocol: transferProtocol,
QueryParams: magellan.QueryParams{ QueryParams: magellan.QueryParams{
Drivers: []string{"redfish"}, Drivers: []string{"redfish"},
Preferred: "redfish", Preferred: "redfish",
Protocol: protocol, Protocol: protocol,
Host: host, Host: host,
User: user, User: user,
Pass: pass, Pass: pass,
Timeout: timeout, Timeout: timeout,
Port: port, Port: port,
WithSecureTLS: withSecureTLS,
}, },
} }
// check if required params are set // check if required params are set
if host == "" || user == "" || pass == "" { if host == "" || user == "" || pass == "" {
l.Log.Fatal("requires host, user, and pass to be set") l.Log.Fatal("requires host, user, and pass to be set")
@ -54,7 +53,7 @@ var updateCmd = &cobra.Command{
} }
return return
} }
// client, err := magellan.NewClient(l, &q.QueryParams) // client, err := magellan.NewClient(l, &q.QueryParams)
// if err != nil { // if err != nil {
// l.Log.Errorf("could not make client: %v", err) // l.Log.Errorf("could not make client: %v", err)
@ -77,7 +76,6 @@ func init() {
updateCmd.Flags().StringVar(&firmwareUrl, "firmware-url", "", "set the path to the firmware") 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(&firmwareVersion, "firmware-version", "", "set the version of firmware to be installed")
updateCmd.Flags().StringVar(&component, "component", "", "set the component to upgrade") 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") updateCmd.Flags().BoolVar(&status, "status", false, "get the status of the update")
viper.BindPFlag("bmc-host", updateCmd.Flags().Lookup("bmc-host")) viper.BindPFlag("bmc-host", updateCmd.Flags().Lookup("bmc-host"))
@ -93,4 +91,4 @@ func init() {
viper.BindPFlag("status", updateCmd.Flags().Lookup("status")) viper.BindPFlag("status", updateCmd.Flags().Lookup("status"))
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
} }

12
go.mod
View file

@ -1,4 +1,4 @@
module github.com/bikeshack/magellan module github.com/OpenCHAMI/magellan
go 1.20 go 1.20
@ -6,9 +6,12 @@ require (
github.com/Cray-HPE/hms-xname v1.3.0 github.com/Cray-HPE/hms-xname v1.3.0
github.com/bmc-toolbox/bmclib/v2 v2.0.1-0.20230714152943-a1b87e2ff47f github.com/bmc-toolbox/bmclib/v2 v2.0.1-0.20230714152943-a1b87e2ff47f
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 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/jacobweinstock/registrar v0.4.7
github.com/jmoiron/sqlx v1.3.5 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/mattn/go-sqlite3 v1.14.6
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.17.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/bmc-toolbox/common v0.0.0-20230717121556-5eb9915a8a5a // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.4 // 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/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
@ -30,6 +34,12 @@ require (
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.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/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect

View file

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

View file

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

View file

@ -12,10 +12,10 @@ import (
"sync" "sync"
"time" "time"
"github.com/bikeshack/magellan/internal/log" "github.com/OpenCHAMI/magellan/internal/log"
"github.com/bikeshack/magellan/internal/api/smd" "github.com/OpenCHAMI/magellan/internal/api/smd"
"github.com/bikeshack/magellan/internal/util" "github.com/OpenCHAMI/magellan/internal/util"
"github.com/Cray-HPE/hms-xname/xnames" "github.com/Cray-HPE/hms-xname/xnames"
bmclib "github.com/bmc-toolbox/bmclib/v2" bmclib "github.com/bmc-toolbox/bmclib/v2"
@ -33,27 +33,27 @@ const (
HTTPS_PORT = 443 HTTPS_PORT = 443
) )
// NOTE: ...params were getting too long... // NOTE: ...params were getting too long...
type QueryParams struct { type QueryParams struct {
Host string Host string
Port int Port int
Protocol string Protocol string
User string User string
Pass string Pass string
Drivers []string Drivers []string
Threads int Threads int
Preferred string Preferred string
Timeout int Timeout int
WithSecureTLS bool CaCertPath string
CertPoolFile string Verbose bool
Verbose bool IpmitoolPath string
IpmitoolPath string OutputPath string
OutputPath string ForceUpdate bool
ForceUpdate bool AccessToken string
} }
func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) { func NewClient(l *log.Logger, q *QueryParams) (*bmclib.Client, error) {
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 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 // only work if valid cert is provided
if q.WithSecureTLS && q.CertPoolFile != "" { if q.CaCertPath != "" {
pool := x509.NewCertPool() pool := x509.NewCertPool()
data, err := os.ReadFile(q.CertPoolFile) data, err := os.ReadFile(q.CaCertPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not read cert pool file: %v", err) 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) done := make(chan struct{}, q.Threads+1)
chanProbeState := make(chan ScannedResult, 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 // collect bmc information asynchronously
var offset = 0
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(q.Threads) wg.Add(q.Threads)
for i := 0; i < q.Threads; i++ { 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.Host = ps.Host
q.Port = ps.Port q.Port = ps.Port
client, err := NewClient(l, q) // generate custom xnames for bmcs
if err != nil { node := xnames.Node{
l.Log.Errorf("could not make client: %v", err) Cabinet: 1000,
continue 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 to be sent to smd
data := map[string]any{ data := map[string]any{
"ID": fmt.Sprintf("%v", node.String()[:len(node.String())-2]), "ID": fmt.Sprintf("%v", node.String()[:len(node.String())-2]),
"Type": "", "Type": "",
"Name": "", "Name": "",
"FQDN": ps.Host, "FQDN": ps.Host,
"User": q.User, "User": q.User,
"Password": q.Pass, "Password": q.Pass,
"MACRequired": true, "MACRequired": true,
"RediscoverOnUpdate": false, "RediscoverOnUpdate": false,
} }
@ -168,96 +172,72 @@ func CollectAll(probeStates *[]ScannedResult, l *log.Logger, q *QueryParams) err
var rm map[string]json.RawMessage var rm map[string]json.RawMessage
// inventories // inventories
inventory, err := CollectInventory(client, q) // if bmclibClient != nil {
if err != nil { // inventory, err := CollectInventory(bmclibClient, q)
l.Log.Errorf("could not query inventory (%v:%v): %v", q.Host, q.Port, err) // 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"] // json.Unmarshal(inventory, &rm)
// data["Inventory"] = rm["Inventory"]
// }
// chassis // chassis
chassis, err := CollectChassis(q) if gofishClient != nil {
if err != nil { chassis, err := CollectChassis(gofishClient, q)
l.Log.Errorf("could not query chassis: %v", err) if err != nil {
continue 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 := make(map[string]string)
headers["Content-Type"] = "application/json" 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 { if err != nil {
l.Log.Errorf("could not marshal JSON: %v", err) l.Log.Errorf("could not marshal JSON: %v", err)
} }
if q.Verbose {
fmt.Printf("%v\n", string(body))
}
// write JSON data to file // 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 { if err != nil {
l.Log.Errorf("could not write data to file: %v", err) l.Log.Errorf("could not write data to file: %v", err)
} }
// add all endpoints to smd // add all endpoints to smd
err = smd.AddRedfishEndpoint(b, headers) err = smd.AddRedfishEndpoint(body, headers)
if err != nil { if err != nil {
l.Log.Error(err) l.Log.Error(err)
// try updating instead // try updating instead
if q.ForceUpdate { if q.ForceUpdate {
err = smd.UpdateRedfishEndpoint(data["ID"].(string), b, headers) err = smd.UpdateRedfishEndpoint(data["ID"].(string), body, headers)
if err != nil { if err != nil {
l.Log.Error(err) 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) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
if q.Verbose {
fmt.Printf("Metadata: %v\n", string(b))
}
ctxCancel() ctxCancel()
return b, nil return b, nil
} }
@ -340,7 +317,6 @@ func CollectInventory(client *bmclib.Client, q *QueryParams) ([]byte, error) {
ctxCancel() ctxCancel()
return nil, fmt.Errorf("could not open client: %v", err) return nil, fmt.Errorf("could not open client: %v", err)
} }
defer client.Close(ctx)
inventory, err := client.Inventory(ctx) inventory, err := client.Inventory(ctx)
if err != nil { 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) return nil, fmt.Errorf("could not get inventory: %v", err)
} }
// retrieve inventory data // retrieve inventory data
data := map[string]any{"Inventory": inventory} data := map[string]any{"Inventory": inventory}
b, err := json.MarshalIndent(data, "", " ") 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) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
ctxCancel() ctxCancel()
return b, nil return b, nil
} }
@ -372,7 +344,6 @@ func CollectPowerState(client *bmclib.Client, q *QueryParams) ([]byte, error) {
ctxCancel() ctxCancel()
return nil, fmt.Errorf("could not open client: %v", err) return nil, fmt.Errorf("could not open client: %v", err)
} }
defer client.Close(ctx)
powerState, err := client.GetPowerState(ctx) powerState, err := client.GetPowerState(ctx)
if err != nil { 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) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
ctxCancel() ctxCancel()
return b, nil return b, nil
@ -415,18 +383,14 @@ func CollectUsers(client *bmclib.Client, q *QueryParams) ([]byte, error) {
} }
// retrieve inventory data // retrieve inventory data
data := map[string]any {"Users": users} data := map[string]any{"Users": users}
b, err := json.MarshalIndent(data, "", " ") b, err := json.MarshalIndent(data, "", " ")
if err != nil { if err != nil {
ctxCancel() ctxCancel()
return nil, fmt.Errorf("could not marshal JSON: %v", err) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
// return b, nil
ctxCancel() ctxCancel()
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil 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) // return nil, fmt.Errorf("could not make query: %v", err)
// } // }
b, err := makeRequest(client, client.GetBiosConfiguration, q.Timeout) b, err := makeRequest(client, client.GetBiosConfiguration, q.Timeout)
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, err return b, err
} }
func CollectEthernetInterfaces(client *bmclib.Client, q *QueryParams, systemID string) ([]byte, error) { func CollectEthernetInterfaces(c *gofish.APIClient, q *QueryParams, systemID string) ([]byte, error) {
c, err := connectGofish(q)
if err != nil {
return nil, fmt.Errorf("could not connect to bmc: %v", err)
}
systems, err := c.Service.Systems() systems, err := c.Service.Systems()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not query storage systems (%v:%v): %v", q.Host, q.Port, err) return nil, fmt.Errorf("could not query storage systems (%v:%v): %v", q.Host, q.Port, err)
@ -455,13 +411,13 @@ func CollectEthernetInterfaces(client *bmclib.Client, q *QueryParams, systemID s
var interfaces []*redfish.EthernetInterface var interfaces []*redfish.EthernetInterface
for _, system := range systems { 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 { if err != nil {
continue continue
} }
interfaces = append(interfaces, i...) interfaces = append(interfaces, i...)
} }
if len(interfaces) <= 0 { if len(interfaces) <= 0 {
return nil, fmt.Errorf("could not get ethernet interfaces: %v", err) return nil, fmt.Errorf("could not get ethernet interfaces: %v", err)
} }
@ -472,17 +428,10 @@ func CollectEthernetInterfaces(client *bmclib.Client, q *QueryParams, systemID s
return nil, fmt.Errorf("could not marshal JSON: %v", err) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
// if q.Verbose {
// fmt.Printf("%v\n", string(b))
// }
return b, nil return b, nil
} }
func CollectChassis(q *QueryParams) ([]byte, error) { func CollectChassis(c *gofish.APIClient, 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)
}
chassis, err := c.Service.Chassis() chassis, err := c.Service.Chassis()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not query chassis (%v:%v): %v", q.Host, q.Port, err) 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) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil return b, nil
} }
func CollectStorage(q *QueryParams) ([]byte, error) { func CollectStorage(c *gofish.APIClient, 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)
}
systems, err := c.Service.StorageSystems() systems, err := c.Service.StorageSystems()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not query storage systems (%v:%v): %v", q.Host, q.Port, err) 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{ data := map[string]any{
"Storage": map[string]any{ "Storage": map[string]any{
"Systems": systems, "Systems": systems,
"Services": services, "Services": services,
}, },
} }
@ -527,18 +468,10 @@ func CollectStorage(q *QueryParams) ([]byte, error) {
return nil, fmt.Errorf("could not marshal JSON: %v", err) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil return b, nil
} }
func CollectSystems(client *bmclib.Client, q *QueryParams) ([]byte, error) { func CollectSystems(c *gofish.APIClient, 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)
}
systems, err := c.Service.Systems() systems, err := c.Service.Systems()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not query systems (%v:%v): %v", q.Host, q.Port, err) 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 // query the system's ethernet interfaces
var temp []map[string]any var temp []map[string]any
for _, system := range systems { for _, system := range systems {
interfaces, err := CollectEthernetInterfaces(client, q, system.ID) interfaces, err := CollectEthernetInterfaces(c, q, system.ID)
if err != nil { if err != nil {
continue continue
} }
var i map[string]any var i map[string]any
json.Unmarshal(interfaces, &i) json.Unmarshal(interfaces, &i)
temp = append(temp, map[string]any{ temp = append(temp, map[string]any{
"Data": system, "Data": system,
"EthernetInterfaces": i["EthernetInterfaces"], "EthernetInterfaces": i["EthernetInterfaces"],
}) })
} }
data := map[string]any{"Systems": temp } data := map[string]any{"Systems": temp}
b, err := json.MarshalIndent(data, "", " ") b, err := json.MarshalIndent(data, "", " ")
if err != nil { if err != nil {
return nil, fmt.Errorf("could not marshal JSON: %v", err) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil return b, nil
} }
func CollectRegisteries(q *QueryParams) ([]byte, error) { func CollectRegisteries(c *gofish.APIClient, 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)
}
registries, err := c.Service.Registries() registries, err := c.Service.Registries()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not query storage systems (%v:%v): %v", q.Host, q.Port, err) 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, "", " ") b, err := json.MarshalIndent(data, "", " ")
if err != nil { if err != nil {
return nil, fmt.Errorf("could not marshal JSON: %v", err) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil 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, "", " ") b, err := json.MarshalIndent(data, "", " ")
if err != nil { if err != nil {
return nil, fmt.Errorf("could not marshal JSON: %v", err) return nil, fmt.Errorf("could not marshal JSON: %v", err)
} }
if q.Verbose {
fmt.Printf("%v\n", string(b))
}
return b, nil return b, nil
} }
func connectGofish(q *QueryParams) (*gofish.APIClient, error) { 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) c, err := gofish.Connect(config)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not connect to redfish endpoint: %v", err) return nil, fmt.Errorf("could not connect to redfish endpoint: %v", err)
} }
if c != nil { if c != nil {
c.Service.ProtocolFeaturesSupported = gofish.ProtocolFeaturesSupported{ c.Service.ProtocolFeaturesSupported = gofish.ProtocolFeaturesSupported{
ExpandQuery: gofish.Expand{ ExpandQuery: gofish.Expand{
ExpandAll: true, ExpandAll: true,
Links: true, Links: true,
}, },
} }
} }
return c, err return c, err
} }
func makeGofishConfig(q *QueryParams) gofish.ClientConfig { func makeGofishConfig(q *QueryParams) (gofish.ClientConfig, error) {
url := baseRedfishUrl(q) 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{ return gofish.ClientConfig{
Endpoint: url, Endpoint: url,
Username: q.User, Username: q.User,
Password: q.Pass, Password: q.Pass,
Insecure: !q.WithSecureTLS, Insecure: q.CaCertPath == "",
TLSHandshakeTimeout: q.Timeout, 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) { 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 ( import (
"fmt" "fmt"
magellan "github.com/bikeshack/magellan/internal" magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/jmoiron/sqlx" "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" "sync"
"time" "time"
"github.com/bikeshack/magellan/internal/util" "github.com/OpenCHAMI/magellan/internal/util"
) )
type ScannedResult struct { type ScannedResult struct {
@ -51,7 +51,6 @@ func rawConnect(host string, ports []int, timeout int, keepOpenOnly bool) []Scan
return results return results
} }
func GenerateHosts(subnet string, subnetMask *net.IP) []string { func GenerateHosts(subnet string, subnetMask *net.IP) []string {
if subnet == "" || subnetMask == nil { if subnet == "" || subnetMask == nil {
return nil return nil
@ -68,7 +67,7 @@ func GenerateHosts(subnet string, subnetMask *net.IP) []string {
subnetIp = ip subnetIp = ip
if network != nil { if network != nil {
t := net.IP(network.Mask) t := net.IP(network.Mask)
subnetMask = &t subnetMask = &t
} }
} }
@ -82,7 +81,7 @@ func generateHosts(ip *net.IP, mask *net.IPMask) []string {
// get all IP addresses in network // get all IP addresses in network
ones, _ := mask.Size() ones, _ := mask.Size()
hosts := []string{} 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++ { for i := 0; i < end; i++ {
// ip[3] = byte(i) // ip[3] = byte(i)
ip = util.GetNextIP(ip, 1) ip = util.GetNextIP(ip, 1)

View file

@ -10,30 +10,29 @@ import (
"strings" "strings"
"time" "time"
"github.com/bikeshack/magellan/internal/log" "github.com/OpenCHAMI/magellan/internal/log"
"github.com/bikeshack/magellan/internal/util" "github.com/OpenCHAMI/magellan/internal/util"
bmclib "github.com/bmc-toolbox/bmclib/v2" bmclib "github.com/bmc-toolbox/bmclib/v2"
"github.com/bmc-toolbox/bmclib/v2/constants" "github.com/bmc-toolbox/bmclib/v2/constants"
bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type UpdateParams struct { type UpdateParams struct {
QueryParams QueryParams
FirmwarePath string FirmwarePath string
FirmwareVersion string FirmwareVersion string
Component string Component string
TransferProtocol string TransferProtocol string
} }
// NOTE: Does not work since OpenBMC, whic bmclib uses underneath, does not // NOTE: Does not work since OpenBMC, whic bmclib uses underneath, does not
// support multipart updates. See issue: https://github.com/bmc-toolbox/bmclib/issues/341 // support multipart updates. See issue: https://github.com/bmc-toolbox/bmclib/issues/341
func UpdateFirmware(client *bmclib.Client, l *log.Logger, q *UpdateParams) error { func UpdateFirmware(client *bmclib.Client, l *log.Logger, q *UpdateParams) error {
if q.Component == "" { if q.Component == "" {
return fmt.Errorf("component is required") return fmt.Errorf("component is required")
} }
// open BMC session and update driver registry // open BMC session and update driver registry
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout)) ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(q.Timeout))
client.Registry.FilterForCompatible(ctx) client.Registry.FilterForCompatible(ctx)
@ -121,20 +120,20 @@ func UpdateFirmware(client *bmclib.Client, l *log.Logger, q *UpdateParams) error
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
return nil return nil
} }
func UpdateFirmwareRemote(q *UpdateParams) error { func UpdateFirmwareRemote(q *UpdateParams) error {
url := baseRedfishUrl(&q.QueryParams) + "/redfish/v1/UpdateService/Actions/SimpleUpdate" url := baseRedfishUrl(&q.QueryParams) + "/redfish/v1/UpdateService/Actions/SimpleUpdate"
headers := map[string]string { headers := map[string]string{
"Content-Type": "application/json", "Content-Type": "application/json",
"cache-control": "no-cache", "cache-control": "no-cache",
} }
b := map[string]any{ b := map[string]any{
"UpdateComponent": q.Component, // BMC, BIOS "UpdateComponent": q.Component, // BMC, BIOS
"TransferProtocol": q.TransferProtocol, "TransferProtocol": q.TransferProtocol,
"ImageURI": q.FirmwarePath, "ImageURI": q.FirmwarePath,
} }
data, err := json.Marshal(b) data, err := json.Marshal(b)
if err != nil { if err != nil {
@ -184,12 +183,10 @@ func GetUpdateStatus(q *UpdateParams) error {
// return fmt.Errorf("could not read file: %v", err) // return fmt.Errorf("could not read file: %v", err)
// } // }
// switch q.TransferProtocol { // switch q.TransferProtocol {
// case "HTTP": // case "HTTP":
// default: // default:
// return fmt.Errorf("transfer protocol not supported") // return fmt.Errorf("transfer protocol not supported")
// } // }
// return nil // return nil
// } // }

View file

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