diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d5109a2..1136387 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,13 +21,6 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - - name: Install dependencies - run: | - sudo apt update && sudo apt install -y curl git gcc g++ make \ - gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ - libc6-dev-arm64-cross software-properties-common - - name: Checkout uses: actions/checkout@v4 @@ -60,6 +53,6 @@ jobs: args: release --clean id: goreleaser - name: Attest Binaries - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@v1 with: - subject-checksums: dist/checksums.txt + subject-path: '${{ github.workspace }}/dist/magellan_linux_amd64_v3/magellan' diff --git a/.github/workflows/prbuild.yml b/.github/workflows/prbuild.yml index 6621214..476ba60 100644 --- a/.github/workflows/prbuild.yml +++ b/.github/workflows/prbuild.yml @@ -20,13 +20,6 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - - name: Install dependencies - run: | - sudo apt update && sudo apt install -y curl git gcc g++ make \ - gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ - libc6-dev-arm64-cross software-properties-common - - name: Checkout uses: actions/checkout@v4 with: @@ -48,4 +41,4 @@ jobs: with: version: '~> v2' args: release --snapshot - id: goreleaser + id: goreleaser \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 04df1f4..24419e5 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -27,20 +27,18 @@ builds: - version goos: - linux + - darwin + - windows goarch: - amd64 - arm64 goamd64: - v3 - goarm: - - 7 env: - - CGO_ENABLED=1 - - CC={{ if eq .Arch "arm64" }}aarch64-linux-gnu-gcc{{ else }}gcc{{ end }} - - CXX={{ if eq .Arch "arm64" }}aarch64-linux-gnu-g++{{ else }}g++{{ end }} + - CGO_ENABLED=0 archives: - - formats: [ 'tar.gz' ] + - format: tar.gz # this name template makes the OS and Arch compatible with the results of uname. name_template: >- {{ .ProjectName }}_ @@ -56,22 +54,23 @@ archives: - magellan.1 nfpms: - - id: magellan - formats: - - deb - - rpm - - apk - - archlinux - maintainer: "David J. Allen " - description: "Magellan is a discovery tool for BMCs." - homepage: "https://www.openchami.org" - license: MIT - section: utils - priority: optional - contents: - - src: magellan.1 - dst: /usr/share/man/man1/ - + - id: magellan + formats: + - deb + - rpm + - apk + - archlinux + maintainer: "David J. Allen " + description: "Magellan is a discovery tool for BMCs." + homepage: "https://www.davidallendj.org" + license: MIT + section: utils + priority: optional + contents: + - src: dist/magellan_{{ .Os }}_{{ if eq .Arch "amd64" }}{{ .Arch }}_{{ .Amd64 }}{{ else }}{{ .Arch }}{{ end }}/magellan + dst: /usr/local/bin/magellan + - src: magellan.1 + dst: /usr/share/man/man1/ dockers: @@ -94,7 +93,7 @@ dockers: - CHANGELOG.md - README.md - image_templates: - - &arm64v7_linux_image ghcr.io/davidallendj/{{.ProjectName}}:{{ .Tag }}-arm64 + - &arm64v8_linux_image ghcr.io/davidallendj/{{.ProjectName}}:{{ .Tag }}-arm64 - ghcr.io/davidallendj/{{.ProjectName}}:{{ .Major }}-arm64 - ghcr.io/davidallendj/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}-arm64 use: buildx @@ -115,22 +114,25 @@ docker_manifests: - name_template: "ghcr.io/davidallendj/{{.ProjectName}}:latest" image_templates: - *amd64_linux_image - - *arm64v7_linux_image + - *arm64v8_linux_image - name_template: "ghcr.io/davidallendj/{{.ProjectName}}:{{ .Tag }}" image_templates: - *amd64_linux_image - - *arm64v7_linux_image + - *arm64v8_linux_image - name_template: "ghcr.io/davidallendj/{{.ProjectName}}:{{ .Major }}" image_templates: - *amd64_linux_image - - *arm64v7_linux_image + - *arm64v8_linux_image - name_template: "ghcr.io/davidallendj/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}" image_templates: - *amd64_linux_image - - *arm64v7_linux_image + - *arm64v8_linux_image + + + checksum: name_template: 'checksums.txt' diff --git a/Makefile b/Makefile index a28a664..5495893 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ mod: ## go mod tidy inst: ## go install tools $(call print-target) go install github.com/client9/misspell/cmd/misspell@v0.3.4 - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.1 + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2 go install github.com/goreleaser/goreleaser/v2@v2.3.2 go install github.com/cpuguy83/go-md2man/v2@latest @@ -71,7 +71,7 @@ build: ## go build container: ## docker build container: $(call print-target) - docker build . --build-arg REGISTRY_HOST=${REGISTRY_HOST} --no-cache --pull --tag '${NAME}:${VERSION}' + docker build . --build-arg REGISTRY_HOST=${REGISTRY_HOST} --no-cache --pull --tag '${NAME}:${VERSION}' .PHONY: spell spell: ## misspell diff --git a/README.md b/README.md index a71f0a0..369f38f 100644 --- a/README.md +++ b/README.md @@ -2,32 +2,7 @@ The `magellan` CLI tool is a Redfish-based, board management controller (BMC) discovery tool designed to scan networks and is written in Go. The tool collects information from BMC nodes using the provided Redfish RESTful API with [`gofish`](https://github.com/stmcginnis/gofish) and loads the queried data into an [SMD](https://github.com/davidallendj/smd/tree/master) instance. The tool strives to be more flexible by implementing multiple methods of discovery to work for a wider range of systems (WIP) and is capable of using independently of other tools or services. -> [!NOTE] -> The v0.1.0 version of `magellan` is incompatible with `smd` v2.15.3 and earlier due to `smd` lacking the inventory parsing code used with `magellan`'s output.** - - - - * [Main Features](#main-features) - * [Getting Started](#getting-started) - * [Building the Executable](#building-the-executable) - + [Building on Debian 12 (Bookworm)](#building-on-debian-12-bookworm) - + [Docker](#docker) - + [Arch Linux (AUR)](#arch-linux-aur) - * [Usage](#usage) - + [Checking for Redfish](#checking-for-redfish) - + [Running the Tool](#running-the-tool) - + [Managing Secrets](#managing-secrets) - + [Starting the Emulator](#starting-the-emulator) - + [Updating Firmware](#updating-firmware) - + [Getting an Access Token (WIP)](#getting-an-access-token-wip) - + [Running with Docker](#running-with-docker) - * [How It Works](#how-it-works) - * [TODO](#todo) - * [Copyright](#copyright) - - - - +**Note: `magellan` v0.1.0 is incompatible with SMD v2.15.3 and earlier.** ## Main Features @@ -38,7 +13,6 @@ The `magellan` tool comes packed with a handleful of features for doing discover - Redfish-based firmware updating - Integration with davidallendj SMD - Write inventory data to JSON -- Store and manage BMC secrets See the [TODO](#todo) section for a list of soon-ish goals planned. @@ -66,7 +40,7 @@ Getting the `magellan` tool to work with Go 1.21 on Debian 12 may require instal apt install gcc golang-1.21/bookworm-backport ``` -The binary executable for the `golang-1.21` executable can then be found using `dpkg`.v2.0.1 +The binary executable for the `golang-1.21` executable can then be found using `dpkg`. ```bash dpkg -L golang-1.21-go @@ -75,7 +49,7 @@ dpkg -L golang-1.21-go Using the correct binary, set the `CGO_ENABLED` environment variable and build the executable with `cgo` enabled: ```bash -export GOBIN=/usr/bin/golang-1.21/bin/go +export GOBIN=/usr/bin/golang-1.21/bin/go go env -w CGO_ENABLED=1 go mod tidy && go build ``` @@ -92,17 +66,6 @@ docker pull ghcr.io/davidallendj/magellan:latest See the ["Running with Docker"](#running-with-docker) section below about running with the Docker container. - -### Arch Linux (AUR) - -The `magellan` tool is in the AUR as a binary package and can be installed via your favorite AUR helper. - -```bash -yay -S magellan-bin -``` -> [!NOTE] -> The AUR package may not always be in sync with the latest release. It is recommended to install `magellan` from source for the latest version. - ## Usage The sections below assume that the BMC nodes have an IP address available to query Redfish. Currently, `magellan` does not support discovery with MAC addresses although that may change in the future. @@ -210,101 +173,14 @@ This will initiate a crawler that will find as much inventory data as possible. Note: If the `cache` flag is not set, `magellan` will use `/tmp/$USER/magellan.db` by default. -### Managing Secrets - -When connecting to an array of BMC nodes, some nodes may have different secret credentials than the rest. These secrets can be stored and used automatically by `magellan` when performing a `collect` or a `crawl`. All secrets are encrypted and are only accessible using the same `MASTER_KEY` as when stored originally. - -To store secrets using `magellan`: - -1. Set the `MASTER_KEY` environment variable. This can be generated using `magellan secrets generatekey`. - -```bash -export MASTER_KEY=$(magellan secrets generatekey) -``` - -2. Store secret credentials for hosts shown by `magellan list`: - -```bash -export bmc_host=https://172.16.0.105:443 -magellan secrets store $bmc_host $bmc_username:$bmc_password -``` - -There should be no output unless an error occurred. - -3. Print the list of hosts to confirm secrets are stored. - -```bash -magellan secrets list -``` - -If you see your `bmc_host` listed in the output, that means that your secrets were stored successfully. - -Additionally, if you want to see the actually contents, make sure the `MASTER_KEY` environment variable is correctly set and do the following: - -```bash -magellan secrets retrieve $bmc_host -``` - -4. Run either a `crawl` or `collect` and `magellan` should be a do find the credentials for each host. - -```bash -magellan crawl -i $bmc_host -magellan collect \ - --username $default_bmc_username \ - --password $default_bmc_password -``` - -If you pass arguments with the `--username/--password` flags, the arguments will override all credentials set in the secret store for each flag. However, it is possible only override a single flag (e.g. `magellan collect --username`). - -> [!NOTE] -> Make sure that the `secretID` is EXACTLY as show with `magellan list`. Otherwise, `magellan` will not be able to do the lookup from the secret store correctly. - -> [!TIP] -> You can set default fallback credentials by storing a secret with the `secretID` of "default". This is used if no `secretID` is found in the local store for the specified host. This is useful when you want to set a username and password that is the same for all BMCs with the exception of the ones specified. -> ```bash -> magellan secrets default $username:$password -> ``` - -### Starting the Emulator - -This repository includes a quick and dirty way to test `magellan` using a Redfish emulator with little to no effort to get running. - -1. Make sure you have `docker` with Docker compose and optionally `make`. - -2. Run the `emulator/setup.sh` script or alternatively `make emulator`. - -This will start a flask server that you can make requests to using `curl`. - -```bash -export emulator_host=https://172.21.0.2:5000 -export emulator_username=root # set in the `rf_emulator.yml` file -export emulator_password=root_password # set in the `rf_emulator.yml` file -curl -k $emulator_host/redfish/v1 -u $emulator_username:$emulator_password -``` - -...or with `magellan` using the secret store... - -```bash -magellan scan --subnet 172.21.0.0/24 -magellan secrets store \ - $emulator_host \ - $emulator_username:$emulator_password -magellan collect --host https://smd.openchami.cluster -``` - -This example should work just like running on real hardware. - -> [!NOTE] -> The emulator host may be different from the one in the README. Make sure to double-check the host! - ### Updating Firmware -The `magellan` tool is capable of updating firmware with using the `update` subcommand via the Redfish API. This may sometimes necessary if some of the `collect` output is missing or is not including what is expected. The subcommand expects there to be a running HTTP/HTTPS server running that has an accessible URL path to the firmware download. Specify the URL with the `--firmware-path` flag and the firmware type with the `--component` flag (optional) with all the other usual arguments like in the example below: +The `magellan` tool is capable of updating firmware with using the `update` subcommand via the Redfish API. This may sometimes necessary if some of the `collect` output is missing or is not including what is expected. The subcommand expects there to be a running HTTP/HTTPS server running that has an accessible URL path to the firmware download. Specify the URL with the `--firmware-path` flag and the firmware type with the `--component` flag with all the other usual arguments like in the example below: ```bash ./magellan update 172.16.0.108:443 \ - --username $bmc_username \ - --password $bmc_password \ + --username $USERNAME \ + --password $PASSWORD \ --firmware-path http://172.16.0.255:8005/firmware/bios/image.RBU \ --component BIOS ``` @@ -312,12 +188,9 @@ The `magellan` tool is capable of updating firmware with using the `update` subc Then, the update status can be viewed by including the `--status` flag along with the other usual arguments or with the `watch` command: ```bash -./magellan update 172.16.0.110 \ - --status \ - --username $bmc_username \ - --password $bmc_password | jq '.' +./magellan update 172.16.0.110 --status --username $USERNAME --pass $PASSWORD | jq '.' # ...or... -watch -n 1 "./magellan update 172.16.0.110 --status --username $bmc_username --password $bmc_password | jq '.'" +watch -n 1 "./magellan update 172.16.0.110 --status --username $USERNAME --password $PASSWORD | jq '.'" ``` ### Getting an Access Token (WIP) @@ -385,10 +258,8 @@ See the [issue list](https://github.com/davidallendj/magellan/issues) for plans * [ ] Separate `collect` subcommand with making request to endpoint * [X] Support logging in with `opaal` to get access token * [X] Support using CA certificates with HTTP requests to SMD -* [X] Add tests for the regressions and compatibility +* [ ] Add tests for the regressions and compatibility * [X] Clean up, remove unused, and tidy code (first round) -* [X] Add `secrets` command to manage secret credentials -* [ ] Add server component to make `magellan` a micro-service ## Copyright diff --git a/build-in-container.sh b/build-in-container.sh deleted file mode 100644 index ba07c82..0000000 --- a/build-in-container.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# This script uses the latest Ubuntu 24.04 container to build the project with GoReleaser. It emulates the GitHub Actions environment as closely as possible. -# Before submitting a PR for release/build. please run this script to ensure your PR will pass the build. - -# Name of the container -CONTAINER_NAME="goreleaser-build" - -# Directory where built binaries will be available -OUTPUT_DIR="$(pwd)/dist" - -export GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi) -export BUILD_HOST=$(hostname) -export GO_VERSION=$(go version | awk '{print $3}') -export BUILD_USER=$(whoami) - -# Start a new disposable Ubuntu 24.04 container with the current directory mounted -${CONTAINER_CMD:-docker} run --rm -it \ - --name "$CONTAINER_NAME" \ - -v "$(pwd)":/workspace \ - -w /workspace \ - ubuntu:24.04 bash -c " - - # Suppress timezone prompts - export DEBIAN_FRONTEND=noninteractive - export TZ=UTC - - - # Update package lists and install dependencies - apt update && apt install -y curl git gcc g++ make \ - gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ - libc6-dev-arm64-cross software-properties-common - - # Install Go (match GitHub runner version) - curl -fsSL https://golang.org/dl/go1.21.5.linux-amd64.tar.gz | tar -C /usr/local -xz - export PATH=\$PATH:/usr/local/go/bin - go version # Verify Go installation - - # Set GOPATH and update PATH to include Go binaries - export GOPATH=\$(go env GOPATH) - export PATH=\$PATH:\$GOPATH/bin - echo \"GOPATH: \$GOPATH\" && echo \"PATH: \$PATH\"`` - - # Install Goreleaser - curl -sL https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz | tar -xz -C /usr/local/bin - goreleaser --version # Verify Goreleaser installation - - # Set Build Environment Variables - export GIT_STATE="$GIT_STATE" - export BUILD_HOST="$BUILD_HOST" - export BUILD_USER="$BUILD_USER" - export GO_VERSION=$(go version | awk '{print $3}') - - # Run Goreleaser - goreleaser build --snapshot --clean -" - -# Notify user of success -echo "✅ Build complete! Check the output in: $OUTPUT_DIR" - diff --git a/cmd/cache.go b/cmd/cache.go index f39bffd..11c87a5 100644 --- a/cmd/cache.go +++ b/cmd/cache.go @@ -6,8 +6,8 @@ import ( "os" "strconv" + magellan "github.com/davidallendj/magellan/internal" "github.com/davidallendj/magellan/internal/cache/sqlite" - magellan "github.com/davidallendj/magellan/pkg" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) diff --git a/cmd/collect.go b/cmd/collect.go index c3fb35c..53807c3 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -1,17 +1,14 @@ package cmd import ( - "encoding/json" "fmt" "os/user" "github.com/cznic/mathutil" + magellan "github.com/davidallendj/magellan/internal" "github.com/davidallendj/magellan/internal/cache/sqlite" urlx "github.com/davidallendj/magellan/internal/url" - magellan "github.com/davidallendj/magellan/pkg" "github.com/davidallendj/magellan/pkg/auth" - "github.com/davidallendj/magellan/pkg/bmc" - "github.com/davidallendj/magellan/pkg/secrets" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -21,19 +18,13 @@ import ( // This command should be ran after the `scan` to find available hosts // on a subnet. var CollectCmd = &cobra.Command{ - Use: "collect", - Example: ` // basic collect after scan without making a follow-up request - magellan collect --cache ./assets.db --cacert ochami.pem -o ./logs -t 30 - - // set username and password for all nodes and make request to specified host - magellan collect --host https://smd.openchami.cluster -u $bmc_username -p $bmc_password - - // run a collect using secrets manager with fallback username and password - export MASTER_KEY=$(magellan secrets generatekey) - magellan secrets store $node_creds_json -f nodes.json - magellan collect --host https://smd.openchami.cluster -u $fallback_bmc_username -p $fallback_bmc_password`, + Use: "collect", Short: "Collect system information by interrogating BMC node", - Long: "Send request(s) to a collection of hosts running Redfish services found stored from the 'scan' in cache.\nSee the 'scan' command on how to perform a scan.", + Long: "Send request(s) to a collection of hosts running Redfish services found stored from the 'scan' in cache.\n" + + "See the 'scan' command on how to perform a scan.\n\n" + + "Examples:\n" + + " magellan collect --cache ./assets.db --output ./logs --timeout 30 --cacert cecert.pem\n" + + " magellan collect --host smd.example.com --port 27779 --username username --password password", Run: func(cmd *cobra.Command, args []string) { // get probe states stored in db from scan scannedResults, err := sqlite.GetScannedAssets(cachePath) @@ -56,67 +47,18 @@ var CollectCmd = &cobra.Command{ } } - // set the minimum/maximum number of concurrent processes + if verbose { + log.Debug().Str("Access Token", accessToken) + } + + // if concurrency <= 0 { concurrency = mathutil.Clamp(len(scannedResults), 1, 10000) } - - // use secret store for BMC credentials, and/or credential CLI flags - var store secrets.SecretStore - if username != "" && password != "" { - // First, try and load credentials from --username and --password if both are set. - log.Debug().Msgf("--username and --password specified, using them for BMC credentials") - store = secrets.NewStaticStore(username, password) - } else { - // Alternatively, locate specific credentials (falling back to default) and override those - // with --username or --password if either are passed. - log.Debug().Msgf("one or both of --username and --password NOT passed, attempting to obtain missing credentials from secret store at %s", secretsFile) - if store, err = secrets.OpenStore(secretsFile); err != nil { - log.Error().Err(err).Msg("failed to open local secrets store") - } - - // Temporarily override username/password of each BMC if one of those - // flags is passed. The expectation is that if the flag is specified - // on the command line, it should be used. - if username != "" { - log.Info().Msg("--username passed, temporarily overriding all usernames from secret store with value") - } - if password != "" { - log.Info().Msg("--password passed, temporarily overriding all passwords from secret store with value") - } - switch s := store.(type) { - case *secrets.StaticStore: - if username != "" { - s.Username = username - } - if password != "" { - s.Password = password - } - case *secrets.LocalSecretStore: - for k, _ := range s.Secrets { - if creds, err := bmc.GetBMCCredentials(store, k); err != nil { - log.Error().Str("id", k).Err(err).Msg("failed to override BMC credentials") - } else { - if username != "" { - creds.Username = username - } - if password != "" { - creds.Password = password - } - - if newCreds, err := json.Marshal(creds); err != nil { - log.Error().Str("id", k).Err(err).Msg("failed to override BMC credentials: marshal error") - } else { - s.StoreSecretByID(k, string(newCreds)) - } - } - } - } - } - - // set the collect parameters from CLI params - params := &magellan.CollectParams{ + err = magellan.CollectInventory(&scannedResults, &magellan.CollectParams{ URI: host, + Username: username, + Password: password, Timeout: timeout, Concurrency: concurrency, Verbose: verbose, @@ -124,35 +66,31 @@ var CollectCmd = &cobra.Command{ OutputPath: outputPath, ForceUpdate: forceUpdate, AccessToken: accessToken, - SecretStore: store, - } - - // show all of the 'collect' parameters being set from CLI if verbose - if verbose { - log.Debug().Any("params", params) - } - - _, err = magellan.CollectInventory(&scannedResults, params) + }) if err != nil { - log.Error().Err(err).Msg("failed to collect data") + log.Error().Err(err).Msgf("failed to collect data") } }, } func init() { currentUser, _ = user.Current() - CollectCmd.Flags().StringVar(&host, "host", "", "Set the URI to the SMD root endpoint") - CollectCmd.Flags().StringVarP(&username, "username", "u", "", "Set the master BMC username") - CollectCmd.Flags().StringVarP(&password, "password", "p", "", "Set the master BMC password") - CollectCmd.Flags().StringVar(&secretsFile, "secrets-file", "", "Set path to the node secrets file") - CollectCmd.Flags().StringVar(&scheme, "scheme", "https", "Set the default scheme used to query when not included in URI") - CollectCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the protocol used to query") - CollectCmd.Flags().StringVarP(&outputPath, "output", "o", fmt.Sprintf("/tmp/%smagellan/inventory/", currentUser.Username+"/"), "Set the path to store collection data") - CollectCmd.Flags().BoolVar(&forceUpdate, "force-update", false, "Set flag to force update data sent to SMD") - CollectCmd.Flags().StringVar(&cacertPath, "cacert", "", "Set the path to CA cert file. (defaults to system CAs when blank)") + CollectCmd.PersistentFlags().StringVar(&host, "host", "", "Set the URI to the SMD root endpoint") + CollectCmd.PersistentFlags().StringVar(&username, "username", "", "Set the BMC user") + CollectCmd.PersistentFlags().StringVar(&password, "password", "", "Set the BMC password") + CollectCmd.PersistentFlags().StringVar(&scheme, "scheme", "https", "Set the scheme used to query") + CollectCmd.PersistentFlags().StringVar(&protocol, "protocol", "tcp", "Set the protocol used to query") + CollectCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", fmt.Sprintf("/tmp/%smagellan/inventory/", currentUser.Username+"/"), "Set the path to store collection data") + CollectCmd.PersistentFlags().BoolVar(&forceUpdate, "force-update", false, "Set flag to force update data sent to SMD") + CollectCmd.PersistentFlags().StringVar(&cacertPath, "cacert", "", "Path to CA cert. (defaults to system CAs)") + + // set flags to only be used together + CollectCmd.MarkFlagsRequiredTogether("username", "password") // bind flags to config properties checkBindFlagError(viper.BindPFlag("collect.host", CollectCmd.Flags().Lookup("host"))) + checkBindFlagError(viper.BindPFlag("collect.username", CollectCmd.Flags().Lookup("username"))) + checkBindFlagError(viper.BindPFlag("collect.password", CollectCmd.Flags().Lookup("password"))) checkBindFlagError(viper.BindPFlag("collect.scheme", CollectCmd.Flags().Lookup("scheme"))) checkBindFlagError(viper.BindPFlag("collect.protocol", CollectCmd.Flags().Lookup("protocol"))) checkBindFlagError(viper.BindPFlag("collect.output", CollectCmd.Flags().Lookup("output"))) diff --git a/cmd/crawl.go b/cmd/crawl.go index c1acc3a..a5bb56c 100644 --- a/cmd/crawl.go +++ b/cmd/crawl.go @@ -3,13 +3,10 @@ package cmd import ( "encoding/json" "fmt" - - "github.com/rs/zerolog/log" + "log" urlx "github.com/davidallendj/magellan/internal/url" - "github.com/davidallendj/magellan/pkg/bmc" "github.com/davidallendj/magellan/pkg/crawler" - "github.com/davidallendj/magellan/pkg/secrets" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -18,11 +15,13 @@ import ( // specfic inventory detail. This command only expects host names and does // not require a scan to be performed beforehand. var CrawlCmd = &cobra.Command{ - Use: "crawl [uri]", - Example: ` magellan crawl https://bmc.example.com - magellan crawl https://bmc.example.com -i -u username -p password`, + Use: "crawl [uri]", Short: "Crawl a single BMC for inventory information", - Long: "Crawl a single BMC for inventory information with URI.\n\n NOTE: This command does not scan subnets, store scan information in cache, nor make a request to a specified host. It is used only to retrieve inventory data directly. Otherwise, use 'scan' and 'collect' instead.", + Long: "Crawl a single BMC for inventory information. This command does NOT store information\n" + + "about the scan into cache after completion. To do so, use the 'collect' command instead\n\n" + + "Examples:\n" + + " magellan crawl https://bmc.example.com\n" + + " magellan crawl https://bmc.example.com -i -u username -p password", Args: func(cmd *cobra.Command, args []string) error { // Validate that the only argument is a valid URI var err error @@ -36,59 +35,19 @@ var CrawlCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { - var ( - uri = args[0] - store secrets.SecretStore - err error - ) - - if username != "" && password != "" { - // First, try and load credentials from --username and --password if both are set. - log.Debug().Str("id", uri).Msgf("--username and --password specified, using them for BMC credentials") - store = secrets.NewStaticStore(username, password) - } else { - // Alternatively, locate specific credentials (falling back to default) and override those - // with --username or --password if either are passed. - log.Debug().Str("id", uri).Msgf("one or both of --username and --password NOT passed, attempting to obtain missing credentials from secret store at %s", secretsFile) - if store, err = secrets.OpenStore(secretsFile); err != nil { - log.Error().Str("id", uri).Err(err).Msg("failed to open local secrets store") - } - - // Either none of the flags were passed or only one of them were; get - // credentials from secrets store to fill in the gaps. - bmcCreds, _ := bmc.GetBMCCredentials(store, uri) - nodeCreds := secrets.StaticStore{ - Username: bmcCreds.Username, - Password: bmcCreds.Password, - } - - // If either of the flags were passed, override the fetched - // credentials with them. - if username != "" { - log.Info().Str("id", uri).Msg("--username was set, overriding username for this BMC") - nodeCreds.Username = username - } - if password != "" { - log.Info().Str("id", uri).Msg("--password was set, overriding password for this BMC") - nodeCreds.Password = password - } - - store = &nodeCreds - } - systems, err := crawler.CrawlBMCForSystems(crawler.CrawlerConfig{ - URI: uri, - CredentialStore: store, - Insecure: insecure, - UseDefault: true, + URI: args[0], + Username: cmd.Flag("username").Value.String(), + Password: cmd.Flag("password").Value.String(), + Insecure: cmd.Flag("insecure").Value.String() == "true", }) if err != nil { - log.Error().Err(err).Msg("failed to crawl BMC") + log.Fatalf("Error crawling BMC: %v", err) } // Marshal the inventory details to JSON jsonData, err := json.MarshalIndent(systems, "", " ") if err != nil { - log.Error().Err(err).Msg("failed to marshal JSON") + fmt.Println("Error marshalling to JSON:", err) return } @@ -98,11 +57,12 @@ var CrawlCmd = &cobra.Command{ } func init() { - CrawlCmd.Flags().StringVarP(&username, "username", "u", "", "Set the username for the BMC") - CrawlCmd.Flags().StringVarP(&password, "password", "p", "", "Set the password for the BMC") - CrawlCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Ignore SSL errors") - CrawlCmd.Flags().StringVarP(&secretsFile, "file", "f", "nodes.json", "set the secrets file with BMC credentials") + CrawlCmd.Flags().StringP("username", "u", "", "Set the username for the BMC") + CrawlCmd.Flags().StringP("password", "p", "", "Set the password for the BMC") + CrawlCmd.Flags().BoolP("insecure", "i", false, "Ignore SSL errors") + checkBindFlagError(viper.BindPFlag("crawl.username", CrawlCmd.Flags().Lookup("username"))) + checkBindFlagError(viper.BindPFlag("crawl.password", CrawlCmd.Flags().Lookup("password"))) checkBindFlagError(viper.BindPFlag("crawl.insecure", CrawlCmd.Flags().Lookup("insecure"))) rootCmd.AddCommand(CrawlCmd) diff --git a/cmd/list.go b/cmd/list.go index 7d5b781..24945ae 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -20,15 +20,13 @@ var ( // and stored in a cache database from a scan. The data that's stored // is what is consumed by the `collect` command with the --cache flag. var ListCmd = &cobra.Command{ - Use: "list", - Example: ` magellan list - magellan list --cache ./assets.db - magellan list --cache-info - `, - Args: cobra.ExactArgs(0), + Use: "list", Short: "List information stored in cache from a scan", Long: "Prints all of the host and associated data found from performing a scan.\n" + - "See the 'scan' command on how to perform a scan.", + "See the 'scan' command on how to perform a scan.\n\n" + + "Examples:\n" + + " magellan list\n" + + " magellan list --cache ./assets.db", Run: func(cmd *cobra.Command, args []string) { // check if we just want to show cache-related info and exit if showCache { diff --git a/cmd/root.go b/cmd/root.go index fc15150..778e37f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -44,7 +44,6 @@ var ( verbose bool debug bool forceUpdate bool - insecure bool ) // The `root` command doesn't do anything on it's own except display @@ -52,7 +51,7 @@ var ( var rootCmd = &cobra.Command{ Use: "magellan", Short: "Redfish-based BMC discovery tool", - Long: "Redfish-based BMC discovery tool with dynamic discovery features.", + Long: "", Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { err := cmd.Help() @@ -75,8 +74,8 @@ func Execute() { func init() { currentUser, _ = user.Current() cobra.OnInitialize(InitializeConfig) - rootCmd.PersistentFlags().IntVarP(&concurrency, "concurrency", "j", -1, "Set the number of concurrent processes") - rootCmd.PersistentFlags().IntVarP(&timeout, "timeout", "t", 5, "Set the timeout for requests") + rootCmd.PersistentFlags().IntVar(&concurrency, "concurrency", -1, "Set the number of concurrent processes") + rootCmd.PersistentFlags().IntVar(&timeout, "timeout", 5, "Set the timeout for requests") rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Set the config file path") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Set to enable/disable verbose output") rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Set to enable/disable debug messages") @@ -94,7 +93,7 @@ func init() { func checkBindFlagError(err error) { if err != nil { - log.Error().Err(err).Msg("failed to bind cobra/viper flag") + log.Error().Err(err).Msg("failed to bind flag") } } diff --git a/cmd/scan.go b/cmd/scan.go index f25150d..d2cbaf8 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -7,8 +7,8 @@ import ( "os" "path" + magellan "github.com/davidallendj/magellan/internal" "github.com/davidallendj/magellan/internal/cache/sqlite" - magellan "github.com/davidallendj/magellan/pkg" "github.com/rs/zerolog/log" "github.com/cznic/mathutil" @@ -33,28 +33,7 @@ var ( // See the `ScanForAssets()` function in 'internal/scan.go' for details // related to the implementation. var ScanCmd = &cobra.Command{ - Use: "scan urls...", - Example: ` - // assumes host https://10.0.0.101:443 - magellan scan 10.0.0.101 - - // assumes subnet using HTTPS and port 443 except for specified host - magellan scan http://10.0.0.101:80 https://user:password@10.0.0.102:443 http://172.16.0.105:8080 --subnet 172.16.0.0/24 - - // assumes hosts http://10.0.0.101:8080 and http://10.0.0.102:8080 - magellan scan 10.0.0.101 10.0.0.102 https://172.16.0.10:443 --port 8080 --protocol tcp - - // assumes subnet using default unspecified subnet-masks - magellan scan --subnet 10.0.0.0 - - // assumes subnet using HTTPS and port 443 with specified CIDR - magellan scan --subnet 10.0.0.0/16 - - // assumes subnet using HTTP and port 5000 similar to 192.168.0.0/16 - magellan scan --subnet 192.168.0.0 --protocol tcp --scheme https --port 5000 --subnet-mask 255.255.0.0 - - // assumes subnet without CIDR has a subnet-mask of 255.255.0.0 - magellan scan --subnet 10.0.0.0/24 --subnet 172.16.0.0 --subnet-mask 255.255.0.0 --cache ./assets.db`, + Use: "scan urls...", Short: "Scan to discover BMC nodes on a network", Long: "Perform a net scan by attempting to connect to each host and port specified and getting a response.\n" + "Each host is passed *with a full URL* including the protocol and port. Additional subnets can be added\n" + @@ -67,7 +46,22 @@ var ScanCmd = &cobra.Command{ "'--protocol' flag.\n\n" + "If the '--disable-probe` flag is used, the tool will not send another request to probe for available.\n" + "Redfish services. This is not recommended, since the extra request makes the scan a bit more reliable\n" + - "for determining which hosts to collect inventory data.\n\n", + "for determining which hosts to collect inventory data.\n\n" + + "Examples:\n" + + // assumes host https://10.0.0.101:443 + " magellan scan 10.0.0.101\n" + + // assumes subnet using HTTPS and port 443 except for specified host + " magellan scan http://10.0.0.101:80 https://user:password@10.0.0.102:443 http://172.16.0.105:8080 --subnet 172.16.0.0/24\n" + + // assumes hosts http://10.0.0.101:8080 and http://10.0.0.102:8080 + " magellan scan 10.0.0.101 10.0.0.102 https://172.16.0.10:443 --port 8080 --protocol tcp\n" + + // assumes subnet using default unspecified subnet-masks + " magellan scan --subnet 10.0.0.0\n" + + // assumes subnet using HTTPS and port 443 with specified CIDR + " magellan scan --subnet 10.0.0.0/16\n" + + // assumes subnet using HTTP and port 5000 similar to 192.168.0.0/16 + " magellan scan --subnet 192.168.0.0 --protocol tcp --scheme https --port 5000 --subnet-mask 255.255.0.0\n" + + // assumes subnet without CIDR has a subnet-mask of 255.255.0.0 + " magellan scan --subnet 10.0.0.0/24 --subnet 172.16.0.0 --subnet-mask 255.255.0.0 --cache ./assets.db\n", Run: func(cmd *cobra.Command, args []string) { // add default ports for hosts if none are specified with flag if len(ports) == 0 { diff --git a/cmd/secrets.go b/cmd/secrets.go deleted file mode 100644 index 59523e6..0000000 --- a/cmd/secrets.go +++ /dev/null @@ -1,277 +0,0 @@ -package cmd - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "os" - "strings" - - "github.com/davidallendj/magellan/pkg/secrets" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var ( - secretsFile string - secretsStoreFormat string - secretsStoreInputFile string -) - -var secretsCmd = &cobra.Command{ - Use: "secrets", - Example: ` - // generate new key and set environment variable - export MASTER_KEY=$(magellan secrets generatekey) - - // store specific BMC node creds for collect and crawl in default secrets store (--file/-f flag not set) - magellan secrets store $bmc_host $bmc_creds - - // retrieve creds from secrets store - magellan secrets retrieve $bmc_host -f nodes.json - - // list creds from specific secrets - magellan secrets list -f nodes.json`, - Short: "Manage credentials for BMC nodes", - Long: "Manage credentials for BMC nodes to for querying information through redfish. This requires generating a key and setting the 'MASTER_KEY' environment variable for the secrets store.", - Run: func(cmd *cobra.Command, args []string) { - // show command help and exit - if len(args) < 1 { - cmd.Help() - os.Exit(0) - } - }, -} - -var secretsGenerateKeyCmd = &cobra.Command{ - Use: "generatekey", - Args: cobra.NoArgs, - Short: "Generates a new 32-byte master key (in hex).", - Run: func(cmd *cobra.Command, args []string) { - key, err := secrets.GenerateMasterKey() - if err != nil { - fmt.Printf("Error generating master key: %v\n", err) - os.Exit(1) - } - fmt.Printf("%s\n", key) - }, -} - -var secretsStoreCmd = &cobra.Command{ - Use: "store secretID ", - Args: cobra.MinimumNArgs(1), - Short: "Stores the given string value under secretID.", - Run: func(cmd *cobra.Command, args []string) { - var ( - secretID = args[0] - secretValue string - store secrets.SecretStore - inputFileBytes []byte - err error - ) - - // require either the args or input file - if len(args) < 1 && secretsStoreInputFile == "" { - log.Error().Msg("no input data or file") - os.Exit(1) - } else if len(args) > 1 && secretsStoreInputFile == "" { - // use args[1] here because args[0] is the secretID - secretValue = args[1] - } - - // handle input file format - switch secretsStoreFormat { - case "basic": // format: $username:$password - var ( - values []string - username string - password string - ) - // seperate username and password provided - values = strings.Split(secretValue, ":") - if len(values) != 2 { - log.Error().Msgf("expected 2 arguments in [username:password] format but got %d", len(values)) - os.Exit(1) - } - - // open secret store to save credentials - store, err = secrets.OpenStore(secretsFile) - if err != nil { - log.Error().Err(err).Msg("failed to open secrets store") - os.Exit(1) - } - - // extract username/password from input (for clarity) - username = values[0] - password = values[1] - - // create JSON formatted string from input - secretValue = fmt.Sprintf("{\"username\": \"%s\", \"password\": \"%s\"}", username, password) - - case "base64": // format: ($encoded_base64_string) - decoded, err := base64.StdEncoding.DecodeString(secretValue) - if err != nil { - log.Error().Err(err).Msg("error decoding base64 data") - os.Exit(1) - } - - // check the decoded string if it's a valid JSON and has creds - if !isValidCredsJSON(string(decoded)) { - log.Error().Err(err).Msg("value is not a valid JSON or is missing credentials") - os.Exit(1) - } - - store, err = secrets.OpenStore(secretsFile) - if err != nil { - log.Error().Err(err).Msg("failed to open secrets store") - os.Exit(1) - } - secretValue = string(decoded) - case "json": // format: {"username": $username, "password": $password} - // read input from file if set and override - if secretsStoreInputFile != "" { - if secretValue != "" { - log.Error().Msg("cannot use -i/--input-file with positional argument") - os.Exit(1) - } - inputFileBytes, err = os.ReadFile(secretsStoreInputFile) - if err != nil { - log.Error().Err(err).Msg("failed to read input file") - os.Exit(1) - } - secretValue = string(inputFileBytes) - } - - // make sure we have valid JSON with "username" and "password" properties - if !isValidCredsJSON(string(secretValue)) { - log.Error().Err(err).Msg("not a valid JSON or creds") - os.Exit(1) - } - store, err = secrets.OpenStore(secretsFile) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - default: - log.Error().Msg("no input format set") - os.Exit(1) - } - - if err := store.StoreSecretByID(secretID, secretValue); err != nil { - fmt.Printf("Error storing secret: %v\n", err) - os.Exit(1) - } - }, -} - -func isValidCredsJSON(val string) bool { - var ( - valid = !json.Valid([]byte(val)) - creds map[string]string - err error - ) - err = json.Unmarshal([]byte(val), &creds) - if err != nil { - return false - } - _, valid = creds["username"] - _, valid = creds["password"] - return valid -} - -var secretsRetrieveCmd = &cobra.Command{ - Use: "retrieve secretID", - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - var ( - secretID = args[0] - secretValue string - store secrets.SecretStore - err error - ) - - store, err = secrets.OpenStore(secretsFile) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - secretValue, err = store.GetSecretByID(secretID) - if err != nil { - fmt.Printf("Error retrieving secret: %v\n", err) - os.Exit(1) - } - fmt.Printf("Secret for %s: %s\n", secretID, secretValue) - }, -} - -var secretsListCmd = &cobra.Command{ - Use: "list", - Args: cobra.ExactArgs(0), - Short: "Lists all the secret IDs and their values.", - Run: func(cmd *cobra.Command, args []string) { - store, err := secrets.OpenStore(secretsFile) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - secrets, err := store.ListSecrets() - if err != nil { - fmt.Printf("Error listing secrets: %v\n", err) - os.Exit(1) - } - - for key, value := range secrets { - fmt.Printf("%s: %s\n", key, value) - } - }, -} - -var secretsRemoveCmd = &cobra.Command{ - Use: "remove secretIDs...", - Args: cobra.MinimumNArgs(1), - Short: "Remove secrets by IDs from secret store.", - Run: func(cmd *cobra.Command, args []string) { - for _, secretID := range args { - // open secret store from file - store, err := secrets.OpenStore(secretsFile) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - // remove secret from store by it's ID - err = store.RemoveSecretByID(secretID) - if err != nil { - fmt.Println("failed to remove secret: ", err) - os.Exit(1) - } - - // update store by saving to original file - secrets.SaveSecrets(secretsFile, store.(*secrets.LocalSecretStore).Secrets) - } - }, -} - -func init() { - secretsCmd.Flags().StringVarP(&secretsFile, "file", "f", "nodes.json", "set the secrets file with BMC credentials") - secretsStoreCmd.Flags().StringVarP(&secretsStoreFormat, "format", "F", "basic", "set the input format for the secrets file (basic|json|base64)") - secretsStoreCmd.Flags().StringVarP(&secretsStoreInputFile, "input-file", "i", "", "set the file to read as input") - - secretsCmd.AddCommand(secretsGenerateKeyCmd) - secretsCmd.AddCommand(secretsStoreCmd) - secretsCmd.AddCommand(secretsRetrieveCmd) - secretsCmd.AddCommand(secretsListCmd) - secretsCmd.AddCommand(secretsRemoveCmd) - - rootCmd.AddCommand(secretsCmd) - - checkBindFlagError(viper.BindPFlags(secretsCmd.Flags())) - checkBindFlagError(viper.BindPFlags(secretsGenerateKeyCmd.Flags())) - checkBindFlagError(viper.BindPFlags(secretsStoreCmd.Flags())) - checkBindFlagError(viper.BindPFlags(secretsGenerateKeyCmd.Flags())) - checkBindFlagError(viper.BindPFlags(secretsGenerateKeyCmd.Flags())) - checkBindFlagError(viper.BindPFlags(secretsGenerateKeyCmd.Flags())) -} diff --git a/cmd/update.go b/cmd/update.go index c2cc3a4..d0b050c 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -4,9 +4,7 @@ import ( "os" "strings" - magellan "github.com/davidallendj/magellan/pkg" - "github.com/davidallendj/magellan/pkg/bmc" - "github.com/davidallendj/magellan/pkg/secrets" + magellan "github.com/davidallendj/magellan/internal" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -14,26 +12,23 @@ import ( var ( host string - firmwareUri string + firmwareUrl string + firmwareVersion string + component string transferProtocol string showStatus bool - Insecure bool ) // The `update` command provides an interface to easily update firmware // using Redfish. It also provides a simple way to check the status of // an update in-progress. var UpdateCmd = &cobra.Command{ - Use: "update hosts...", - Example: ` // perform an firmware update - magellan update 172.16.0.108:443 -i -u $bmc_username -p $bmc_password \ - --firmware-url http://172.16.0.200:8005/firmware/bios/image.RBU \ - --component BIOS - - // check update status - magellan update 172.16.0.108:443 -i -u $bmc_username -p $bmc_password --status`, + Use: "update hosts...", Short: "Update BMC node firmware", - Long: "Perform an firmware update using Redfish by providing a remote firmware URL and component.", + Long: "Perform an firmware update using Redfish by providing a remote firmware URL and component.\n\n" + + "Examples:\n" + + " magellan update 172.16.0.108:443 --username bmc_username --password bmc_password --firmware-url http://172.16.0.200:8005/firmware/bios/image.RBU --component BIOS\n" + + " magellan update 172.16.0.108:443 --status --username bmc_username --password bmc_password", Run: func(cmd *cobra.Command, args []string) { // check that we have at least one host if len(args) <= 0 { @@ -41,57 +36,19 @@ var UpdateCmd = &cobra.Command{ os.Exit(1) } - // use secret store for BMC credentials, and/or credential CLI flags - var ( - store secrets.SecretStore - uri = args[0] - err error - ) - if username != "" && password != "" { - // First, try and load credentials from --username and --password if both are set. - log.Debug().Str("id", uri).Msgf("--username and --password specified, using them for BMC credentials") - store = secrets.NewStaticStore(username, password) - } else { - // Alternatively, locate specific credentials (falling back to default) and override those - // with --username or --password if either are passed. - log.Debug().Str("id", uri).Msgf("one or both of --username and --password NOT passed, attempting to obtain missing credentials from secret store at %s", secretsFile) - if store, err = secrets.OpenStore(secretsFile); err != nil { - log.Error().Str("id", uri).Err(err).Msg("failed to open local secrets store") - } - - // Either none of the flags were passed or only one of them were; get - // credentials from secrets store to fill in the gaps. - bmcCreds, _ := bmc.GetBMCCredentials(store, uri) - nodeCreds := secrets.StaticStore{ - Username: bmcCreds.Username, - Password: bmcCreds.Password, - } - - // If either of the flags were passed, override the fetched - // credentials with them. - if username != "" { - log.Info().Str("id", uri).Msg("--username was set, overriding username for this BMC") - nodeCreds.Username = username - } - if password != "" { - log.Info().Str("id", uri).Msg("--password was set, overriding password for this BMC") - nodeCreds.Password = password - } - - store = &nodeCreds - } - // get status if flag is set and exit for _, arg := range args { if showStatus { err := magellan.GetUpdateStatus(&magellan.UpdateParams{ - FirmwareURI: firmwareUri, + FirmwarePath: firmwareUrl, + FirmwareVersion: firmwareVersion, + Component: component, TransferProtocol: transferProtocol, - Insecure: Insecure, CollectParams: magellan.CollectParams{ - URI: arg, - SecretStore: store, - Timeout: timeout, + URI: arg, + Username: username, + Password: password, + Timeout: timeout, }, }) if err != nil { @@ -102,13 +59,15 @@ var UpdateCmd = &cobra.Command{ // initiate a remote update err := magellan.UpdateFirmwareRemote(&magellan.UpdateParams{ - FirmwareURI: firmwareUri, + FirmwarePath: firmwareUrl, + FirmwareVersion: firmwareVersion, + Component: component, TransferProtocol: strings.ToUpper(transferProtocol), - Insecure: Insecure, CollectParams: magellan.CollectParams{ - URI: arg, - SecretStore: store, - Timeout: timeout, + URI: host, + Username: username, + Password: password, + Timeout: timeout, }, }) if err != nil { @@ -122,7 +81,9 @@ func init() { UpdateCmd.Flags().StringVar(&username, "username", "", "Set the BMC user") UpdateCmd.Flags().StringVar(&password, "password", "", "Set the BMC password") UpdateCmd.Flags().StringVar(&transferProtocol, "scheme", "https", "Set the transfer protocol") - UpdateCmd.Flags().StringVar(&firmwareUri, "firmware-uri", "", "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(&component, "component", "", "Set the component to upgrade (BMC|BIOS)") UpdateCmd.Flags().BoolVar(&showStatus, "status", false, "Get the status of the update") checkBindFlagError(viper.BindPFlag("update.username", UpdateCmd.Flags().Lookup("username"))) diff --git a/emulator/rf-emulator.yml b/emulator/rf-emulator.yml index 4b399ea..eec8191 100644 --- a/emulator/rf-emulator.yml +++ b/emulator/rf-emulator.yml @@ -6,7 +6,7 @@ volumes: services: emulator: - image: ghcr.io/openchami/csm-rie:latest + image: davidallendj-rie:latest container_name: rf-emulator environment: BMC_PORT: 5000 diff --git a/go.mod b/go.mod index 1224cc7..1a2e948 100644 --- a/go.mod +++ b/go.mod @@ -16,12 +16,10 @@ require ( golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 ) -require ( - github.com/rs/zerolog v1.33.0 - golang.org/x/crypto v0.32.0 -) +require github.com/rs/zerolog v1.33.0 require ( + github.com/google/go-cmp v0.6.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect ) @@ -30,7 +28,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect @@ -52,8 +49,9 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index be886fb..befbed6 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -151,8 +151,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -164,9 +164,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/cache/sqlite/sqlite.go b/internal/cache/sqlite/sqlite.go index bbbdff3..fa0ae0b 100644 --- a/internal/cache/sqlite/sqlite.go +++ b/internal/cache/sqlite/sqlite.go @@ -2,9 +2,10 @@ package sqlite import ( "fmt" + "strings" + magellan "github.com/davidallendj/magellan/internal" "github.com/davidallendj/magellan/internal/util" - magellan "github.com/davidallendj/magellan/pkg" "github.com/jmoiron/sqlx" ) @@ -27,10 +28,7 @@ func CreateScannedAssetIfNotExists(path string) (*sqlx.DB, error) { if err != nil { return nil, fmt.Errorf("failed to open database: %v", err) } - _, err = db.Exec(schema) - if err != nil { - return nil, fmt.Errorf("failed to create scanned assets cache: %v", err) - } + db.MustExec(schema) return db, nil } @@ -77,7 +75,22 @@ func DeleteScannedAssets(path string, assets ...magellan.RemoteAsset) error { } tx = db.MustBegin() for _, asset := range assets { - sql := fmt.Sprintf(`DELETE FROM %s WHERE host=:host AND port=:port;`, TABLE_NAME) + // skip if neither host nor port are specified + if asset.Host == "" && asset.Port <= 0 { + continue + } + sql := fmt.Sprintf(`DELETE FROM %s`, TABLE_NAME) + where := []string{} + if asset.Port > 0 { + where = append(where, "port=:port") + } + if asset.Host != "" { + where = append(where, "host=:host") + } + if len(where) <= 0 { + continue + } + sql += fmt.Sprintf(" WHERE %s;", strings.Join(where, " AND ")) _, err := tx.NamedExec(sql, &asset) if err != nil { fmt.Printf("failed to execute DELETE transaction: %v\n", err) diff --git a/pkg/collect.go b/internal/collect.go similarity index 77% rename from pkg/collect.go rename to internal/collect.go index d5b8855..2f2c863 100644 --- a/pkg/collect.go +++ b/internal/collect.go @@ -15,10 +15,8 @@ import ( "sync" "time" - "github.com/davidallendj/magellan/pkg/bmc" "github.com/davidallendj/magellan/pkg/client" "github.com/davidallendj/magellan/pkg/crawler" - "github.com/davidallendj/magellan/pkg/secrets" "github.com/rs/zerolog/log" @@ -32,15 +30,16 @@ import ( // CollectParams is a collection of common parameters passed to the CLI // for the 'collect' subcommand. type CollectParams struct { - URI string // set by the 'host' flag - Concurrency int // set the of concurrent jobs with the 'concurrency' flag - Timeout int // set the timeout with the 'timeout' flag - CaCertPath string // set the cert path with the 'cacert' flag - Verbose bool // set whether to include verbose output with 'verbose' flag - OutputPath string // set the path to save output with 'output' flag - ForceUpdate bool // set whether to force updating SMD with 'force-update' flag - AccessToken string // set the access token to include in request with 'access-token' flag - SecretStore secrets.SecretStore // set BMC credentials + URI string // set by the 'host' flag + Username string // set the BMC username with the 'username' flag + Password string // set the BMC password with the 'password' flag + Concurrency int // set the of concurrent jobs with the 'concurrency' flag + Timeout int // set the timeout with the 'timeout' flag + CaCertPath string // set the cert path with the 'cacert' flag + Verbose bool // set whether to include verbose output with 'verbose' flag + OutputPath string // set the path to save output with 'output' flag + ForceUpdate bool // set whether to force updating SMD with 'force-update' flag + AccessToken string // set the access token to include in request with 'access-token' flag } // This is the main function used to collect information from the BMC nodes via Redfish. @@ -49,38 +48,36 @@ type CollectParams struct { // // Requests can be made to several of the nodes using a goroutine by setting the q.Concurrency // property value between 1 and 10000. -func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[string]any, error) { +func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { // check for available remote assets found from scan if assets == nil { - return nil, fmt.Errorf("no assets found") + return fmt.Errorf("no assets found") } if len(*assets) <= 0 { - return nil, fmt.Errorf("no assets found") + return fmt.Errorf("no assets found") } // collect bmc information asynchronously var ( offset = 0 wg sync.WaitGroup - collection = make([]map[string]any, 0) found = make([]string, 0, len(*assets)) done = make(chan struct{}, params.Concurrency+1) chanAssets = make(chan RemoteAsset, params.Concurrency+1) outputPath = path.Clean(params.OutputPath) smdClient = &client.SmdClient{Client: &http.Client{}} ) - // set the client's params from CLI // NOTE: temporary solution until client.NewClient() is fixed smdClient.URI = params.URI if params.CaCertPath != "" { cacert, err := os.ReadFile(params.CaCertPath) if err != nil { - return nil, fmt.Errorf("failed to read CA cert path: %w", err) + return fmt.Errorf("failed to read CA cert path: %w", err) } certPool := x509.NewCertPool() certPool.AppendCertsFromPEM(cacert) - smdClient.Transport = &http.Transport{ + smdClient.Client.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: certPool, InsecureSkipVerify: true, @@ -106,15 +103,12 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin // generate custom xnames for bmcs // TODO: add xname customization via CLI - var ( - uri = fmt.Sprintf("%s:%d", sr.Host, sr.Port) - node = xnames.Node{ - Cabinet: 1000, - Chassis: 1, - ComputeModule: 7, - NodeBMC: offset, - } - ) + node := xnames.Node{ + Cabinet: 1000, + Chassis: 1, + ComputeModule: 7, + NodeBMC: offset, + } offset += 1 // crawl BMC node to fetch inventory data via Redfish @@ -122,33 +116,19 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin systems []crawler.InventoryDetail managers []crawler.Manager config = crawler.CrawlerConfig{ - URI: uri, - CredentialStore: params.SecretStore, - Insecure: true, - UseDefault: true, + URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port), + Username: params.Username, + Password: params.Password, + Insecure: true, } - err error ) - - // crawl for node and BMC information - systems, err = crawler.CrawlBMCForSystems(config) + systems, err := crawler.CrawlBMCForSystems(config) if err != nil { - log.Error().Err(err).Str("uri", uri).Msg("failed to crawl BMC for systems") + log.Error().Err(err).Msg("failed to crawl BMC for systems") } managers, err = crawler.CrawlBMCForManagers(config) if err != nil { - log.Error().Err(err).Str("uri", uri).Msg("failed to crawl BMC for managers") - } - - // we didn't find anything so do not proceed - if len(systems) == 0 && len(managers) == 0 { - continue - } - - // get BMC username to send - bmcCreds := bmc.GetBMCCredentialsOrDefault(params.SecretStore, config.URI) - if bmcCreds == (bmc.BMCCredentials{}) { - log.Warn().Str("id", config.URI).Msg("username will be blank") + log.Error().Err(err).Msg("failed to crawl BMC for managers") } // data to be sent to smd @@ -157,7 +137,7 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin "Type": "", "Name": "", "FQDN": sr.Host, - "User": bmcCreds.Username, + "User": params.Username, "MACRequired": true, "RediscoverOnUpdate": false, "Systems": systems, @@ -189,9 +169,6 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin fmt.Printf("%v\n", string(body)) } - // add data output to collections - collection = append(collection, data) - // write JSON data to file if output path is set using hive partitioning strategy if outputPath != "" { var ( @@ -264,7 +241,7 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin wg.Wait() close(done) - return collection, nil + return nil } // FindMACAddressWithIP() returns the MAC address of an ethernet interface with @@ -279,15 +256,10 @@ func FindMACAddressWithIP(config crawler.CrawlerConfig, targetIP net.IP) (string // gofish (at least for now). If there's a need for grabbing more // manager information in the future, we can move the logic into // the crawler. - bmc_creds, err := config.GetUserPass() - if err != nil { - return "", fmt.Errorf("failed to get credentials for URI: %s", config.URI) - } - client, err := gofish.Connect(gofish.ClientConfig{ Endpoint: config.URI, - Username: bmc_creds.Username, - Password: bmc_creds.Password, + Username: config.Username, + Password: config.Password, Insecure: config.Insecure, BasicAuth: true, }) diff --git a/pkg/scan.go b/internal/scan.go similarity index 99% rename from pkg/scan.go rename to internal/scan.go index c1b5f13..99b4904 100644 --- a/pkg/scan.go +++ b/internal/scan.go @@ -203,7 +203,7 @@ func rawConnect(address string, protocol string, timeoutSeconds int, keepOpenOnl ) // try to conntect to host (expects host in format [10.0.0.0]:443) - target := net.JoinHostPort(uri.Hostname(), uri.Port()) + target := fmt.Sprintf("%s:%s", uri.Hostname(), uri.Port()) conn, err := net.DialTimeout(protocol, target, timeoutDuration) if err != nil { asset.State = false diff --git a/internal/update.go b/internal/update.go new file mode 100644 index 0000000..464b0b9 --- /dev/null +++ b/internal/update.go @@ -0,0 +1,78 @@ +package magellan + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/davidallendj/magellan/pkg/client" +) + +type UpdateParams struct { + CollectParams + FirmwarePath string + FirmwareVersion string + Component string + TransferProtocol string +} + +// UpdateFirmwareRemote() uses 'gofish' to update the firmware of a BMC node. +// The function expects the firmware URL, firmware version, and component flags to be +// set from the CLI to perform a firmware update. +func UpdateFirmwareRemote(q *UpdateParams) error { + // parse URI to set up full address + uri, err := url.ParseRequestURI(q.URI) + if err != nil { + return fmt.Errorf("failed to parse URI: %w", err) + } + uri.User = url.UserPassword(q.Username, q.Password) + + // set up other vars + updateUrl := fmt.Sprintf("%s/redfish/v1/UpdateService/Actions/SimpleUpdate", uri.String()) + headers := map[string]string{ + "Content-Type": "application/json", + "cache-control": "no-cache", + } + b := map[string]any{ + "UpdateComponent": q.Component, // BMC, BIOS + "TransferProtocol": q.TransferProtocol, + "ImageURI": q.FirmwarePath, + } + data, err := json.Marshal(b) + if err != nil { + return fmt.Errorf("failed to marshal data: %v", err) + } + res, body, err := client.MakeRequest(nil, updateUrl, "POST", data, headers) + if err != nil { + return fmt.Errorf("something went wrong: %v", err) + } else if res == nil { + return fmt.Errorf("no response returned (url: %s)", updateUrl) + } + if len(body) > 0 { + fmt.Printf("%d: %v\n", res.StatusCode, string(body)) + } + return nil +} + +func GetUpdateStatus(q *UpdateParams) error { + // parse URI to set up full address + uri, err := url.ParseRequestURI(q.URI) + if err != nil { + return fmt.Errorf("failed to parse URI: %w", err) + } + uri.User = url.UserPassword(q.Username, q.Password) + updateUrl := fmt.Sprintf("%s/redfish/v1/UpdateService", uri.String()) + res, body, err := client.MakeRequest(nil, updateUrl, "GET", nil, nil) + if err != nil { + return fmt.Errorf("something went wrong: %v", err) + } else if res == nil { + return fmt.Errorf("no response returned (url: %s)", updateUrl) + } else if res.StatusCode != http.StatusOK { + return fmt.Errorf("returned status code %d", res.StatusCode) + } + if len(body) > 0 { + fmt.Printf("%v\n", string(body)) + } + return nil +} diff --git a/internal/util/bmc.go b/internal/util/bmc.go deleted file mode 100644 index d2aaebf..0000000 --- a/internal/util/bmc.go +++ /dev/null @@ -1,47 +0,0 @@ -package util - -import ( - "github.com/davidallendj/magellan/pkg/bmc" - "github.com/davidallendj/magellan/pkg/secrets" - "github.com/rs/zerolog/log" -) - -func GetBMCCredentials(store secrets.SecretStore, id string) bmc.BMCCredentials { - var ( - creds bmc.BMCCredentials - err error - ) - - if id == "" { - log.Error().Msg("failed to get BMC credentials: id was empty") - return creds - } - - if id == secrets.DEFAULT_KEY { - log.Info().Msg("fetching default credentials") - if creds, err = bmc.GetBMCCredentialsDefault(store); err != nil { - log.Warn().Err(err).Msg("failed to get default credentials") - } else { - log.Info().Msg("default credentials found, using") - } - return creds - } - - if creds, err = bmc.GetBMCCredentials(store, id); err != nil { - // Specific credentials for URI not found, fetch default. - log.Warn().Str("id", id).Msg("specific credentials not found, falling back to default") - if defaultSecret, err := bmc.GetBMCCredentialsDefault(store); err != nil { - // We've exhausted all options, the credentials will be blank unless - // overridden by a CLI flag. - log.Warn().Str("id", id).Err(err).Msg("no default credentials were set, they will be blank unless overridden by CLI flags") - } else { - // Default credentials found, use them. - log.Info().Str("id", id).Msg("default credentials found, using") - creds = defaultSecret - } - } else { - log.Info().Str("id", id).Msg("specific credentials found, using") - } - - return creds -} diff --git a/pkg/bmc/bmc.go b/pkg/bmc/bmc.go deleted file mode 100644 index 90dcadf..0000000 --- a/pkg/bmc/bmc.go +++ /dev/null @@ -1,65 +0,0 @@ -package bmc - -import ( - "encoding/json" - "fmt" - - "github.com/davidallendj/magellan/pkg/secrets" -) - -type BMCCredentials struct { - Username string `json:"username"` - Password string `json:"password"` -} - -func GetBMCCredentialsDefault(store secrets.SecretStore) (BMCCredentials, error) { - var creds BMCCredentials - if strCreds, err := store.GetSecretByID(secrets.DEFAULT_KEY); err != nil { - return creds, fmt.Errorf("get default BMC credentials from secret store: %w", err) - } else { - // Default URI credentials found, use them. - if err = json.Unmarshal([]byte(strCreds), &creds); err != nil { - return creds, fmt.Errorf("get default BMC credentials from secret store: failed to unmarshal: %w", err) - } - return creds, nil - } -} - -func GetBMCCredentials(store secrets.SecretStore, id string) (BMCCredentials, error) { - var creds BMCCredentials - if strCreds, err := store.GetSecretByID(id); err != nil { - return creds, fmt.Errorf("get BMC credentials from secret store: %w", err) - } else { - // Specific URI credentials found, use them. - if err = json.Unmarshal([]byte(strCreds), &creds); err != nil { - return creds, fmt.Errorf("get BMC credentials from secret store: failed to unmarshal: %w", err) - } - } - - return creds, nil -} - -func GetBMCCredentialsOrDefault(store secrets.SecretStore, id string) BMCCredentials { - var ( - creds BMCCredentials - err error - ) - - if id == "" { - return creds - } - - if id == secrets.DEFAULT_KEY { - creds, _ = GetBMCCredentialsDefault(store) - return creds - } - - if creds, err = GetBMCCredentials(store, id); err != nil { - if defaultSecret, err := GetBMCCredentialsDefault(store); err == nil { - // Default credentials found, use them. - creds = defaultSecret - } - } - - return creds -} diff --git a/pkg/crawler/identify.go b/pkg/crawler/identify.go deleted file mode 100644 index be5b788..0000000 --- a/pkg/crawler/identify.go +++ /dev/null @@ -1,69 +0,0 @@ -package crawler - -import ( - "fmt" - - "github.com/stmcginnis/gofish" - "github.com/stmcginnis/gofish/redfish" -) - -// BMCInfo represents relevant information about a BMC -type BMCInfo struct { - Manufacturer string `json:"manufacturer"` - Model string `json:"model"` - SerialNumber string `json:"serial_number"` - FirmwareVersion string `json:"firmware_version"` - ManagerType string `json:"manager_type"` - UUID string `json:"uuid"` -} - -// IsBMC checks if a given Manager is a BMC based on its type and associations -func IsBMC(manager *redfish.Manager) bool { - if manager == nil { - return false - } - - // Valid BMC types in Redfish - bmcTypes := map[string]bool{ - "BMC": true, - "ManagementController": true, // Some BMCs use this type - } - - // Check if ManagerType matches a BMC type - if !bmcTypes[string(manager.ManagerType)] { - return false - } - - return false // Otherwise, it's likely a chassis manager or other device -} - -// GetBMCInfo retrieves details of all available BMCs -func GetBMCInfo(client *gofish.APIClient) ([]BMCInfo, error) { - var bmcList []BMCInfo - - // Retrieve all managers (BMCs and other managers) - managers, err := client.Service.Managers() - if err != nil { - return nil, fmt.Errorf("failed to retrieve managers: %v", err) - } - - // Iterate through each manager and collect BMC details - for _, manager := range managers { - if !IsBMC(manager) { - continue // Skip if it's not a BMC - } - - bmc := BMCInfo{ - Manufacturer: manager.Manufacturer, - Model: manager.Model, - SerialNumber: manager.SerialNumber, - FirmwareVersion: manager.FirmwareVersion, - ManagerType: string(manager.ManagerType), // Convert ManagerType to string - UUID: manager.UUID, - } - - bmcList = append(bmcList, bmc) - } - - return bmcList, nil -} diff --git a/pkg/crawler/main.go b/pkg/crawler/main.go index 3aa4900..771efb9 100644 --- a/pkg/crawler/main.go +++ b/pkg/crawler/main.go @@ -4,23 +4,16 @@ import ( "fmt" "strings" - "github.com/davidallendj/magellan/internal/util" - "github.com/davidallendj/magellan/pkg/bmc" - "github.com/davidallendj/magellan/pkg/secrets" "github.com/rs/zerolog/log" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/redfish" ) type CrawlerConfig struct { - URI string // URI of the BMC - Insecure bool // Whether to ignore SSL errors - CredentialStore secrets.SecretStore - UseDefault bool -} - -func (cc *CrawlerConfig) GetUserPass() (bmc.BMCCredentials, error) { - return loadBMCCreds(*cc) + URI string // URI of the BMC + Username string // Username for the BMC + Password string // Password for the BMC + Insecure bool // Whether to ignore SSL errors } type EthernetInterface struct { @@ -89,20 +82,11 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) { systems []InventoryDetail rf_systems []*redfish.ComputerSystem ) - // get username and password from secret store - bmc_creds, err := loadBMCCreds(config) - if err != nil { - event := log.Error() - event.Err(err) - event.Msg("failed to load BMC credentials") - return nil, err - } - // initialize gofish client client, err := gofish.Connect(gofish.ClientConfig{ Endpoint: config.URI, - Username: bmc_creds.Username, - Password: bmc_creds.Password, + Username: config.Username, + Password: config.Password, Insecure: config.Insecure, BasicAuth: true, }) @@ -145,40 +129,14 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) { return walkSystems(rf_systems, nil, config.URI) } -// CrawlBMCForManagers connects to a BMC (Baseboard Management Controller) using the provided configuration, -// retrieves the ServiceRoot, and then fetches the list of managers from the ServiceRoot. -// -// Parameters: -// - config: A CrawlerConfig struct containing the URI, username, password, and other connection details. -// -// Returns: -// - []Manager: A slice of Manager structs representing the managers retrieved from the BMC. -// - error: An error object if any error occurs during the connection or retrieval process. -// -// The function performs the following steps: -// 1. Initializes a gofish client with the provided configuration. -// 2. Attempts to connect to the BMC using the gofish client. -// 3. Handles specific connection errors such as 404 (ServiceRoot not found) and 401 (authentication failed). -// 4. Logs out from the client after the operations are completed. -// 5. Retrieves the ServiceRoot from the connected BMC. -// 6. Fetches the list of managers from the ServiceRoot. -// 7. Returns the list of managers and any error encountered during the process. +// CrawlBMCForSystems pulls BMC manager information. func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) { - - // get username and password from secret store - bmc_creds, err := loadBMCCreds(config) - if err != nil { - event := log.Error() - event.Err(err) - event.Msg("failed to load BMC credentials") - return nil, err - } // initialize gofish client var managers []Manager client, err := gofish.Connect(gofish.ClientConfig{ Endpoint: config.URI, - Username: bmc_creds.Username, - Password: bmc_creds.Password, + Username: config.Username, + Password: config.Password, Insecure: config.Insecure, BasicAuth: true, }) @@ -207,27 +165,6 @@ func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) { return walkManagers(rf_managers, config.URI) } -// walkSystems processes a list of Redfish computer systems and their associated chassis, -// and returns a list of inventory details for each system. -// -// Parameters: -// - rf_systems: A slice of pointers to redfish.ComputerSystem objects representing the computer systems to be processed. -// - rf_chassis: A pointer to a redfish.Chassis object representing the chassis associated with the computer systems. -// - baseURI: A string representing the base URI for constructing resource URIs. -// -// Returns: -// - A slice of InventoryDetail objects containing detailed information about each computer system. -// - An error if any issues occur while processing the computer systems or their associated resources. -// -// The function performs the following steps: -// 1. Iterates over each computer system in rf_systems. -// 2. Constructs an InventoryDetail object for each computer system, populating fields such as URI, UUID, Name, Manufacturer, SystemType, Model, Serial, BiosVersion, PowerState, ProcessorCount, ProcessorType, and MemoryTotal. -// 3. If rf_chassis is not nil, populates additional chassis-related fields in the InventoryDetail object. -// 4. Retrieves and processes Ethernet interfaces for each computer system, adding them to the EthernetInterfaces field of the InventoryDetail object. -// 5. Retrieves and processes Network interfaces and their associated network adapters for each computer system, adding them to the NetworkInterfaces field of the InventoryDetail object. -// 6. Processes trusted modules for each computer system, adding them to the TrustedModules field of the InventoryDetail object. -// 7. Appends the populated InventoryDetail object to the systems slice. -// 8. Returns the systems slice and any error encountered during processing. func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chassis, baseURI string) ([]InventoryDetail, error) { systems := []InventoryDetail{} for _, rf_computersystem := range rf_systems { @@ -316,23 +253,6 @@ func walkSystems(rf_systems []*redfish.ComputerSystem, rf_chassis *redfish.Chass return systems, nil } -// walkManagers processes a list of Redfish managers and extracts relevant information -// to create a slice of Manager objects. -// -// Parameters: -// -// rf_managers - A slice of pointers to redfish.Manager objects representing the Redfish managers to be processed. -// baseURI - A string representing the base URI to be used for constructing URIs for the managers and their Ethernet interfaces. -// -// Returns: -// -// A slice of Manager objects containing the extracted information from the provided Redfish managers. -// An error if any issues occur while retrieving Ethernet interfaces from the managers. -// -// The function iterates over each Redfish manager, retrieves its Ethernet interfaces, -// and constructs a Manager object with the relevant details, including Ethernet interface information. -// If an error occurs while retrieving Ethernet interfaces, the function logs the error and returns the managers -// collected so far along with the error. func walkManagers(rf_managers []*redfish.Manager, baseURI string) ([]Manager, error) { var managers []Manager for _, rf_manager := range rf_managers { @@ -368,15 +288,3 @@ func walkManagers(rf_managers []*redfish.Manager, baseURI string) ([]Manager, er } return managers, nil } - -func loadBMCCreds(config CrawlerConfig) (bmc.BMCCredentials, error) { - // NOTE: it is possible for the SecretStore to be nil, so we need a check - if config.CredentialStore == nil { - return bmc.BMCCredentials{}, fmt.Errorf("credential store is invalid") - } - if creds := util.GetBMCCredentials(config.CredentialStore, config.URI); creds == (bmc.BMCCredentials{}) { - return creds, fmt.Errorf("%s: credentials blank for BNC", config.URI) - } else { - return creds, nil - } -} diff --git a/pkg/secrets/encryption.go b/pkg/secrets/encryption.go deleted file mode 100644 index 6faa737..0000000 --- a/pkg/secrets/encryption.go +++ /dev/null @@ -1,75 +0,0 @@ -package secrets - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - - "golang.org/x/crypto/hkdf" -) - -// Derive a unique AES key per SecretID using HKDF -func deriveAESKey(masterKey []byte, secretID string) []byte { - salt := []byte(secretID) - hkdf := hkdf.New(sha256.New, masterKey, salt, nil) - derivedKey := make([]byte, 32) // AES-256 key - io.ReadFull(hkdf, derivedKey) - return derivedKey -} - -// Encrypt data using AES-GCM -func encryptAESGCM(key, plaintext []byte) (string, error) { - block, err := aes.NewCipher(key) - if err != nil { - return "", err - } - - aesGCM, err := cipher.NewGCM(block) - if err != nil { - return "", err - } - - nonce := make([]byte, aesGCM.NonceSize()) - _, err = io.ReadFull(rand.Reader, nonce) - if err != nil { - return "", err - } - - ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) - return hex.EncodeToString(ciphertext), nil -} - -// Decrypt data using AES-GCM -func decryptAESGCM(key []byte, encryptedData string) (string, error) { - data, err := hex.DecodeString(encryptedData) - if err != nil { - return "", err - } - - block, err := aes.NewCipher(key) - if err != nil { - return "", err - } - - aesGCM, err := cipher.NewGCM(block) - if err != nil { - return "", err - } - - nonceSize := aesGCM.NonceSize() - if len(data) < nonceSize { - return "", fmt.Errorf("ciphertext too short") - } - - nonce, ciphertext := data[:nonceSize], data[nonceSize:] - plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) - if err != nil { - return "", err - } - - return string(plaintext), nil -} diff --git a/pkg/secrets/encryption_test.go b/pkg/secrets/encryption_test.go deleted file mode 100644 index bc1919b..0000000 --- a/pkg/secrets/encryption_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package secrets - -import ( - "testing" -) - -func TestDeriveAESKey(t *testing.T) { - masterKey := []byte("testmasterkey") - secretID := "mySecretID" - key1 := deriveAESKey(masterKey, secretID) - key2 := deriveAESKey(masterKey, secretID) - - if len(key1) != 32 { - t.Errorf("derived key should be 32 bytes, got %d", len(key1)) - } - if string(key1) != string(key2) { - t.Errorf("keys derived from same secretID should match") - } -} - -func TestEncryptDecryptAESGCM(t *testing.T) { - masterKey := []byte("anotherTestMasterKey") - secretID := "testSecret" - plaintext := "Hello, secrets!" - - key := deriveAESKey(masterKey, secretID) - - encrypted, err := encryptAESGCM(key, []byte(plaintext)) - if err != nil { - t.Fatalf("encryption failed: %v", err) - } - - decrypted, err := decryptAESGCM(key, encrypted) - if err != nil { - t.Fatalf("decryption failed: %v", err) - } - - if decrypted != plaintext { - t.Errorf("expected %q, got %q", plaintext, decrypted) - } -} diff --git a/pkg/secrets/example/main.go b/pkg/secrets/example/main.go deleted file mode 100644 index d5739b8..0000000 --- a/pkg/secrets/example/main.go +++ /dev/null @@ -1,212 +0,0 @@ -package main - -// This example demonstrates the usage of the LocalSecretStore to store and retrieve secrets. -// It provides a command-line interface to generate a master key, store secrets, and retrieve them. -// The master key is assumed to be stored in the environment variable MASTER_KEY and while it can -// anything you want, we recommend a 32 bit key for AES-256 encryption. The master key is used -// as part of a Key Derivation Function (KDF) to generate a unique AES key for each secret. -// The algorithm of choice is HMAC-based Extract-and-Expand Key Derivation Function (HKDF). -// Each secret is separately encrypted using AES-GCM and stored in a JSON file. -// The JSON file is loaded into memory when the LocalSecretStore is created and saved back to the file -// when a secret is stored or removed. -// - -import ( - "encoding/base64" - "fmt" - "os" - - "github.com/davidallendj/magellan/pkg/secrets" -) - -func usage() { - fmt.Println("Usage:") - fmt.Println(" go run main.go generatekey") - fmt.Println(" - Generates a new 32-byte master key (in hex).") - fmt.Println() - fmt.Println(" Export MASTER_KEY= to use the same key in the next commands.") - fmt.Println() - fmt.Println(" go run main.go store [filename]") - fmt.Println(" - Stores the given string value under secretID.") - fmt.Println() - fmt.Println(" go run main.go storebase64 [filename]") - fmt.Println(" - Decodes the base64-encoded string before storing.") - fmt.Println() - fmt.Println(" go run main.go storejson [filename]") - fmt.Println(" - Stores the provided JSON for the specified secretID.") - fmt.Println() - fmt.Println(" go run main.go retrieve [filename]") - fmt.Println(" - Retrieves and prints the secret value for the given secretID.") - fmt.Println() - fmt.Println(" go run main.go list [filename]") - fmt.Println(" - Lists all the secret IDs and their values.") - fmt.Println() -} - -// openStore tries to create or open the LocalSecretStore based on the environment -// variable MASTER_KEY. If not found, it prints an error. -func openStore(filename string) (*secrets.LocalSecretStore, error) { - masterKey := os.Getenv("MASTER_KEY") - if masterKey == "" { - return nil, fmt.Errorf("MASTER_KEY environment variable not set") - } - - store, err := secrets.NewLocalSecretStore(masterKey, filename, true) - if err != nil { - return nil, fmt.Errorf("cannot open secrets store: %v", err) - } - return store, nil -} - -func main() { - if len(os.Args) < 2 { - usage() - os.Exit(1) - } - - cmd := os.Args[1] - - switch cmd { - case "generatekey": - key, err := secrets.GenerateMasterKey() - if err != nil { - fmt.Printf("Error generating master key: %v\n", err) - os.Exit(1) - } - fmt.Printf("%s\n", key) - - case "store": - if len(os.Args) < 4 { - fmt.Println("Not enough arguments. Usage: go run main.go store [filename]") - os.Exit(1) - } - secretID := os.Args[2] - secretValue := os.Args[3] - filename := "mysecrets.json" - if len(os.Args) == 5 { - filename = os.Args[4] - } - - store, err := openStore(filename) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if err := store.StoreSecretByID(secretID, secretValue); err != nil { - fmt.Printf("Error storing secret: %v\n", err) - os.Exit(1) - } - fmt.Println("Secret stored successfully.") - - case "storebase64": - if len(os.Args) < 4 { - fmt.Println("Not enough arguments. Usage: go run main.go storebase64 [filename]") - os.Exit(1) - } - secretID := os.Args[2] - base64Value := os.Args[3] - filename := "mysecrets.json" - if len(os.Args) == 5 { - filename = os.Args[4] - } - - decoded, err := base64.StdEncoding.DecodeString(base64Value) - if err != nil { - fmt.Printf("Error decoding base64 data: %v\n", err) - os.Exit(1) - } - - store, err := openStore(filename) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if err := store.StoreSecretByID(secretID, string(decoded)); err != nil { - fmt.Printf("Error storing base64-decoded secret: %v\n", err) - os.Exit(1) - } - fmt.Println("Base64-decoded secret stored successfully.") - - case "storejson": - if len(os.Args) < 4 { - fmt.Println(`Not enough arguments. Usage: go run main.go storejson '{"key":"value"}' [filename]`) - os.Exit(1) - } - secretID := os.Args[2] - jsonValue := os.Args[3] - filename := "mysecrets.json" - if len(os.Args) == 5 { - filename = os.Args[4] - } - - store, err := openStore(filename) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if err := store.StoreSecretByID(secretID, jsonValue); err != nil { - fmt.Printf("Error storing JSON secret: %v\n", err) - os.Exit(1) - } - fmt.Println("JSON secret stored successfully.") - - case "retrieve": - if len(os.Args) < 3 { - fmt.Println("Not enough arguments. Usage: go run main.go retrieve [filename]") - os.Exit(1) - } - secretID := os.Args[2] - filename := "mysecrets.json" - if len(os.Args) == 4 { - filename = os.Args[3] - } - - store, err := openStore(filename) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - secretValue, err := store.GetSecretByID(secretID) - if err != nil { - fmt.Printf("Error retrieving secret: %v\n", err) - os.Exit(1) - } - fmt.Printf("Secret for %s: %s\n", secretID, secretValue) - - case "list": - if len(os.Args) < 2 { - fmt.Println("Not enough arguments. Usage: go run main.go list [filename]") - os.Exit(1) - } - - filename := "mysecrets.json" - if len(os.Args) == 3 { - filename = os.Args[2] - } - - store, err := openStore(filename) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - secrets, err := store.ListSecrets() - if err != nil { - fmt.Printf("Error listing secrets: %v\n", err) - os.Exit(1) - } - - fmt.Println("Secrets:") - for key, value := range secrets { - fmt.Printf("%s: %s\n", key, value) - } - - default: - usage() - } - -} diff --git a/pkg/secrets/localstore.go b/pkg/secrets/localstore.go deleted file mode 100644 index f426320..0000000 --- a/pkg/secrets/localstore.go +++ /dev/null @@ -1,161 +0,0 @@ -package secrets - -import ( - "crypto/rand" - "encoding/hex" - "encoding/json" - "fmt" - "os" - "sync" -) - -// Structure to store encrypted secrets in a JSON file -type LocalSecretStore struct { - mu sync.RWMutex - masterKey []byte - filename string - Secrets map[string]string `json:"secrets"` -} - -func NewLocalSecretStore(masterKeyHex, filename string, create bool) (*LocalSecretStore, error) { - var secrets map[string]string - - masterKey, err := hex.DecodeString(masterKeyHex) - if err != nil { - return nil, fmt.Errorf("unable to generate masterkey from hex representation: %v", err) - } - - if _, err := os.Stat(filename); os.IsNotExist(err) { - if !create { - return nil, fmt.Errorf("file %s does not exist", filename) - } - file, err := os.Create(filename) - if err != nil { - return nil, fmt.Errorf("unable to create file %s: %v", filename, err) - } - file.Close() - secrets = make(map[string]string) - } - - if secrets == nil { - secrets, err = loadSecrets(filename) - if err != nil { - return nil, fmt.Errorf("unable to load secrets from file: %v", err) - } - } - - return &LocalSecretStore{ - masterKey: masterKey, - filename: filename, - Secrets: secrets, - }, nil -} - -// GenerateMasterKey creates a 32-byte random key and returns it as a hex string. -func GenerateMasterKey() (string, error) { - key := make([]byte, 32) // 32 bytes for AES-256 - _, err := rand.Read(key) - if err != nil { - return "", err - } - return hex.EncodeToString(key), nil -} - -// GetSecretByID decrypts the secret using the master key and returns it -func (l *LocalSecretStore) GetSecretByID(secretID string) (string, error) { - l.mu.RLock() - encrypted, exists := l.Secrets[secretID] - l.mu.RUnlock() - if !exists { - return "", fmt.Errorf("no secret found for %s", secretID) - } - - derivedKey := deriveAESKey(l.masterKey, secretID) - return decryptAESGCM(derivedKey, encrypted) -} - -// StoreSecretByID encrypts the secret using the master key and stores it in the JSON file -func (l *LocalSecretStore) StoreSecretByID(secretID, secret string) error { - derivedKey := deriveAESKey(l.masterKey, secretID) - encryptedSecret, err := encryptAESGCM(derivedKey, []byte(secret)) - if err != nil { - return err - } - - l.mu.Lock() - l.Secrets[secretID] = encryptedSecret - err = SaveSecrets(l.filename, l.Secrets) - l.mu.Unlock() - return err -} - -// ListSecrets returns a copy of secret IDs to secrets stored in memory -func (l *LocalSecretStore) ListSecrets() (map[string]string, error) { - l.mu.RLock() - defer l.mu.RUnlock() - - secretsCopy := make(map[string]string) - for key, value := range l.Secrets { - secretsCopy[key] = value - } - return secretsCopy, nil -} - -// RemoveSecretByID removes the specified secretID stored locally -func (l *LocalSecretStore) RemoveSecretByID(secretID string) error { - l.mu.RLock() - // Let user know if there was nothing to delete - _, err := l.GetSecretByID(secretID) - if err != nil { - return err - } - delete(l.Secrets, secretID) - l.mu.RUnlock() - return nil -} - -// openStore tries to create or open the LocalSecretStore based on the environment -// variable MASTER_KEY. If not found, it prints an error. -func OpenStore(filename string) (SecretStore, error) { - if filename == "" { - return nil, fmt.Errorf("path to secret store required") - } - - masterKey := os.Getenv("MASTER_KEY") - if masterKey == "" { - return nil, fmt.Errorf("MASTER_KEY environment variable not set") - } - - store, err := NewLocalSecretStore(masterKey, filename, true) - if err != nil { - return nil, fmt.Errorf("failed to create new local secret store: %v", err) - } - return store, nil -} - -// Saves secrets back to the JSON file -func SaveSecrets(jsonFile string, store map[string]string) error { - file, err := os.OpenFile(jsonFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - return err - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - return encoder.Encode(store) -} - -// Loads the secrets JSON file -func loadSecrets(jsonFile string) (map[string]string, error) { - file, err := os.Open(jsonFile) - if err != nil { - return nil, fmt.Errorf("unable to open secret file %s:%v", jsonFile, err) - } - defer file.Close() - - store := make(map[string]string) - decoder := json.NewDecoder(file) - err = decoder.Decode(&store) - return store, err -} diff --git a/pkg/secrets/localstore_test.go b/pkg/secrets/localstore_test.go deleted file mode 100644 index 4009946..0000000 --- a/pkg/secrets/localstore_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package secrets - -import ( - "encoding/hex" - "os" - "testing" -) - -func TestNewLocalSecretStore(t *testing.T) { - masterKey, err := GenerateMasterKey() - if err != nil { - t.Fatalf("Failed to generate master key: %v", err) - } - - filename := "test_secrets.json" - defer os.Remove(filename) - - store, err := NewLocalSecretStore(masterKey, filename, true) - if err != nil { - t.Fatalf("Failed to create LocalSecretStore: %v", err) - } - - if store.filename != filename { - t.Errorf("Expected filename %s, got %s", filename, store.filename) - } - - if hex.EncodeToString(store.masterKey) != masterKey { - t.Errorf("Expected master key %s, got %s", masterKey, hex.EncodeToString(store.masterKey)) - } -} - -func TestGenerateMasterKey(t *testing.T) { - key, err := GenerateMasterKey() - if err != nil { - t.Fatalf("Failed to generate master key: %v", err) - } - - if len(key) != 64 { // 32 bytes in hex representation - t.Errorf("Expected key length 64, got %d", len(key)) - } -} - -func TestStoreAndGetSecretByID(t *testing.T) { - masterKey, err := GenerateMasterKey() - if err != nil { - t.Fatalf("Failed to generate master key: %v", err) - } - - filename := "test_secrets.json" - defer os.Remove(filename) - - store, err := NewLocalSecretStore(masterKey, filename, true) - if err != nil { - t.Fatalf("Failed to create LocalSecretStore: %v", err) - } - - secretID := "test_secret" - secretValue := "my_secret_value" - - err = store.StoreSecretByID(secretID, secretValue) - if err != nil { - t.Fatalf("Failed to store secret: %v", err) - } - - retrievedSecret, err := store.GetSecretByID(secretID) - if err != nil { - t.Fatalf("Failed to get secret: %v", err) - } - - if retrievedSecret != secretValue { - t.Errorf("Expected secret value %s, got %s", secretValue, retrievedSecret) - } -} - -func TestStoreAndGetSecretJSON(t *testing.T) { - masterKey, err := GenerateMasterKey() - if err != nil { - t.Fatalf("Failed to generate master key: %v", err) - } - - filename := "test_secrets.json" - defer os.Remove(filename) - - store, err := NewLocalSecretStore(masterKey, filename, true) - if err != nil { - t.Fatalf("Failed to create LocalSecretStore: %v", err) - } - - secretID := "json_creds" - jsonSecret := `{"username":"testUser","password":"testPass"}` - - if err := store.StoreSecretByID(secretID, jsonSecret); err != nil { - t.Fatalf("Failed to store JSON secret: %v", err) - } - - retrieved, err := store.GetSecretByID(secretID) - if err != nil { - t.Fatalf("Failed to get JSON secret by ID: %v", err) - } - - if retrieved != jsonSecret { - t.Errorf("Expected %s, got %s", jsonSecret, retrieved) - } -} - -func TestListSecrets(t *testing.T) { - masterKey, err := GenerateMasterKey() - if err != nil { - t.Fatalf("Failed to generate master key: %v", err) - } - - filename := "test_secrets.json" - defer os.Remove(filename) - - store, err := NewLocalSecretStore(masterKey, filename, true) - if err != nil { - t.Fatalf("Failed to create LocalSecretStore: %v", err) - } - - secretID1 := "test_secret_1" - secretValue1 := "my_secret_value_1" - secretID2 := "test_secret_2" - secretValue2 := "my_secret_value_2" - - err = store.StoreSecretByID(secretID1, secretValue1) - if err != nil { - t.Fatalf("Failed to store secret: %v", err) - } - - err = store.StoreSecretByID(secretID2, secretValue2) - if err != nil { - t.Fatalf("Failed to store secret: %v", err) - } - - secrets, err := store.ListSecrets() - if err != nil { - t.Fatalf("Failed to list secrets: %v", err) - } - - if len(secrets) != 2 { - t.Errorf("Expected 2 secrets, got %d", len(secrets)) - } - - if secrets[secretID1] != store.Secrets[secretID1] { - t.Errorf("Expected secret value %s, got %s", store.Secrets[secretID1], secrets[secretID1]) - } - - if secrets[secretID2] != store.Secrets[secretID2] { - t.Errorf("Expected secret value %s, got %s", store.Secrets[secretID2], secrets[secretID2]) - } -} diff --git a/pkg/secrets/main.go b/pkg/secrets/main.go deleted file mode 100644 index 983f159..0000000 --- a/pkg/secrets/main.go +++ /dev/null @@ -1,10 +0,0 @@ -package secrets - -const DEFAULT_KEY = "default" - -type SecretStore interface { - GetSecretByID(secretID string) (string, error) - StoreSecretByID(secretID, secret string) error - ListSecrets() (map[string]string, error) - RemoveSecretByID(secretID string) error -} diff --git a/pkg/secrets/staticstore.go b/pkg/secrets/staticstore.go deleted file mode 100644 index 40d9049..0000000 --- a/pkg/secrets/staticstore.go +++ /dev/null @@ -1,35 +0,0 @@ -package secrets - -import "fmt" - -type StaticStore struct { - Username string - Password string -} - -// NewStaticStore creates a new StaticStore with the given username and password. -func NewStaticStore(username, password string) *StaticStore { - return &StaticStore{ - Username: username, - Password: password, - } -} - -func (s *StaticStore) GetSecretByID(secretID string) (string, error) { - return fmt.Sprintf(`{"username":"%s","password":"%s"}`, s.Username, s.Password), nil -} - -func (s *StaticStore) StoreSecretByID(secretID, secret string) error { - return nil -} - -func (s *StaticStore) ListSecrets() (map[string]string, error) { - return map[string]string{ - "static_creds": fmt.Sprintf(`{"username":"%s","password":"%s"}`, s.Username, s.Password), - }, nil -} - -func (s *StaticStore) RemoveSecretByID(secretID string) error { - // Nothing to do here, since nothing is being stored. With different implementations, we could return an error when no secret is found for a specific ID. - return nil -} diff --git a/pkg/update.go b/pkg/update.go deleted file mode 100644 index 838ba0a..0000000 --- a/pkg/update.go +++ /dev/null @@ -1,103 +0,0 @@ -package magellan - -import ( - "fmt" - "net/url" - - "github.com/davidallendj/magellan/pkg/bmc" - "github.com/stmcginnis/gofish" - "github.com/stmcginnis/gofish/redfish" -) - -type UpdateParams struct { - CollectParams - FirmwareURI string - TransferProtocol string - Insecure bool -} - -// UpdateFirmwareRemote() uses 'gofish' to update the firmware of a BMC node. -// The function expects the firmware URL, firmware version, and component flags to be -// set from the CLI to perform a firmware update. -// Example: -// ./magellan update https://192.168.23.40 --username root --password 0penBmc -// --firmware-url http://192.168.23.19:1337/obmc-phosphor-image.static.mtd.tar -// --scheme TFTP -// -// being: -// q.URI https://192.168.23.40 -// q.TransferProtocol TFTP -// q.FirmwarePath http://192.168.23.19:1337/obmc-phosphor-image.static.mtd.tar -func UpdateFirmwareRemote(q *UpdateParams) error { - // parse URI to set up full address - uri, err := url.ParseRequestURI(q.URI) - if err != nil { - return fmt.Errorf("failed to parse URI: %w", err) - } - - // Get BMC credentials from secret store in update parameters - bmcCreds, err := bmc.GetBMCCredentials(q.SecretStore, q.URI) - if err != nil { - return fmt.Errorf("failed to get BMC credentials: %w", err) - } - - // Connect to the Redfish service using gofish - client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: bmcCreds.Username, Password: bmcCreds.Password, Insecure: q.Insecure}) - if err != nil { - return fmt.Errorf("failed to connect to Redfish service: %w", err) - } - defer client.Logout() - - // Retrieve the UpdateService from the Redfish client - updateService, err := client.Service.UpdateService() - if err != nil { - return fmt.Errorf("failed to get update service: %w", err) - } - - // Build the update request payload - req := redfish.SimpleUpdateParameters{ - ImageURI: q.FirmwareURI, - TransferProtocol: redfish.TransferProtocolType(q.TransferProtocol), - } - - // Execute the SimpleUpdate action - err = updateService.SimpleUpdate(&req) - if err != nil { - return fmt.Errorf("firmware update failed: %w", err) - } - fmt.Println("Firmware update initiated successfully.") - return nil -} - -func GetUpdateStatus(q *UpdateParams) error { - // parse URI to set up full address - uri, err := url.ParseRequestURI(q.URI) - if err != nil { - return fmt.Errorf("failed to parse URI: %w", err) - } - - // Get BMC credentials from secret store in update parameters - bmcCreds, err := bmc.GetBMCCredentials(q.SecretStore, q.URI) - if err != nil { - return fmt.Errorf("failed to get BMC credentials: %w", err) - } - - // Connect to the Redfish service using gofish - client, err := gofish.Connect(gofish.ClientConfig{Endpoint: uri.String(), Username: bmcCreds.Username, Password: bmcCreds.Password, Insecure: q.Insecure}) - if err != nil { - return fmt.Errorf("failed to connect to Redfish service: %w", err) - } - defer client.Logout() - - // Retrieve the UpdateService from the Redfish client - updateService, err := client.Service.UpdateService() - if err != nil { - return fmt.Errorf("failed to get update service: %w", err) - } - - // Get the update status - status := updateService.Status - fmt.Printf("Update Status: %v\n", status) - - return nil -} diff --git a/tests/api_test.go b/tests/api_test.go index 572b941..d46a312 100644 --- a/tests/api_test.go +++ b/tests/api_test.go @@ -22,8 +22,8 @@ import ( "flag" + magellan "github.com/davidallendj/magellan/internal" "github.com/davidallendj/magellan/internal/util" - magellan "github.com/davidallendj/magellan/pkg" "github.com/davidallendj/magellan/pkg/client" "github.com/rs/zerolog/log" ) diff --git a/tests/compatibility_test.go b/tests/compatibility_test.go index 766b96b..c947e17 100644 --- a/tests/compatibility_test.go +++ b/tests/compatibility_test.go @@ -16,7 +16,6 @@ import ( "github.com/davidallendj/magellan/pkg/client" "github.com/davidallendj/magellan/pkg/crawler" - "github.com/davidallendj/magellan/pkg/secrets" ) var ( @@ -78,7 +77,7 @@ func TestRedfishV1ServiceRootAvailability(t *testing.T) { // Simple test to ensure an expected Redfish version minimum requirement. func TestRedfishV1Version(t *testing.T) { var ( - url = fmt.Sprintf("%s/redfish/v1/", *host) + url string = fmt.Sprintf("%s/redfish/v1/", *host) body client.HTTPBody = []byte{} headers client.HTTPHeader = map[string]string{} testClient = &http.Client{ @@ -127,17 +126,12 @@ func TestExpectedOutput(t *testing.T) { t.Fatalf("failed while waiting for emulator: %v", err) } - // initialize a credential store - staticStore := &secrets.StaticStore{ - Username: *username, - Password: *password, - } - systems, err := crawler.CrawlBMCForSystems( crawler.CrawlerConfig{ - URI: *host, - CredentialStore: staticStore, - Insecure: true, + URI: *host, + Username: *username, + Password: *password, + Insecure: true, }, )