Repository: bmc-toolbox/bmclib Branch: main Commit: 3269f94932e9 Files: 219 Total size: 1.0 MB Directory structure: gitextract_jzsoh1mg/ ├── .devcontainer/ │ ├── Dockerfile │ └── devcontainer.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── default.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── mergify.yml │ └── workflows/ │ └── ci.yaml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── bmc/ │ ├── bios.go │ ├── bmc.go │ ├── boot_device.go │ ├── boot_device_test.go │ ├── connection.go │ ├── connection_test.go │ ├── firmware.go │ ├── firmware_test.go │ ├── floppy.go │ ├── floppy_test.go │ ├── inventory.go │ ├── inventory_test.go │ ├── nmi.go │ ├── nmi_test.go │ ├── postcode.go │ ├── postcode_test.go │ ├── power.go │ ├── power_test.go │ ├── provider.go │ ├── reset.go │ ├── reset_test.go │ ├── screenshot.go │ ├── screenshot_test.go │ ├── sel.go │ ├── sel_test.go │ ├── sol.go │ ├── sol_test.go │ ├── user.go │ ├── user_test.go │ ├── virtual_media.go │ └── virtual_media_test.go ├── client.go ├── client_test.go ├── constants/ │ └── constants.go ├── doc.go ├── errors/ │ └── errors.go ├── examples/ │ ├── bios/ │ │ ├── doc.go │ │ └── main.go │ ├── create-users/ │ │ ├── doc.go │ │ └── main.go │ ├── floppy-image/ │ │ ├── doc.go │ │ └── main.go │ ├── homeassistant/ │ │ └── main.go │ ├── install-firmware/ │ │ ├── doc.go │ │ └── main.go │ ├── inventory/ │ │ ├── doc.go │ │ ├── main.go │ │ └── output.json │ ├── reset_bmc/ │ │ └── reset_bmc.go │ ├── rpc/ │ │ └── main.go │ ├── screenshot/ │ │ ├── doc.go │ │ └── main.go │ ├── sel/ │ │ └── main.go │ ├── status/ │ │ ├── doc.go │ │ └── main.go │ └── virtualmedia/ │ ├── doc.go │ └── main.go ├── filter.go ├── fixtures/ │ └── internal/ │ └── sum/ │ ├── ChangeBiosConfig │ ├── ChangeBiosConfig-Changed │ ├── ChangeBiosConfig-Changed-Reboot │ ├── GetBIOSInfo │ ├── GetBiosConfiguration │ └── SetBiosConfiguration ├── go.mod ├── go.sum ├── internal/ │ ├── executor/ │ │ ├── errors.go │ │ ├── executor.go │ │ ├── executor_test.go │ │ └── fake_executor.go │ ├── helper/ │ │ ├── helper.go │ │ └── helper_test.go │ ├── httpclient/ │ │ ├── httpclient.go │ │ └── httpclient_test.go │ ├── ipmi/ │ │ └── ipmi.go │ ├── redfishwrapper/ │ │ ├── bios.go │ │ ├── bios_test.go │ │ ├── boot_device.go │ │ ├── client.go │ │ ├── client_test.go │ │ ├── firmware.go │ │ ├── firmware_test.go │ │ ├── fixtures/ │ │ │ ├── dell/ │ │ │ │ ├── bios.json │ │ │ │ ├── manager.idrac.embedded.1.json │ │ │ │ ├── managers.json │ │ │ │ ├── serviceroot.json │ │ │ │ ├── system.embedded.1.json │ │ │ │ ├── system.embedded.1.virtualmedia.json │ │ │ │ ├── systems.json │ │ │ │ ├── virtualmedia_1.json │ │ │ │ ├── virtualmedia_2.json │ │ │ │ └── virtualmedia_collection.json │ │ │ ├── managers.json │ │ │ ├── managers_1.json │ │ │ ├── serviceroot.json │ │ │ ├── serviceroot_no_manager.json │ │ │ ├── smc_1.14.0_serviceroot.json │ │ │ ├── smc_1.14.0_systems.json │ │ │ ├── smc_1.14.0_systems_1.json │ │ │ ├── smc_1.9.0_serviceroot.json │ │ │ ├── systems.json │ │ │ ├── systems_1.json │ │ │ ├── systems_1_no_bios.json │ │ │ ├── systems_bios.json │ │ │ ├── tasks/ │ │ │ │ ├── tasks_1_completed.json │ │ │ │ ├── tasks_1_failed.json │ │ │ │ ├── tasks_1_pending.json │ │ │ │ ├── tasks_1_running.json │ │ │ │ ├── tasks_1_scheduled.json │ │ │ │ ├── tasks_1_starting.json │ │ │ │ ├── tasks_1_unknown.json │ │ │ │ └── tasks_2.json │ │ │ ├── tasks.json │ │ │ ├── taskservice.json │ │ │ ├── updateservice_disabled.json │ │ │ ├── updateservice_ok_response.json │ │ │ ├── updateservice_unexpected_response.json │ │ │ ├── updateservice_with_httppushuri.json │ │ │ └── updateservice_with_multipart.json │ │ ├── inventory.go │ │ ├── inventory_collect.go │ │ ├── inventory_collect_test.go │ │ ├── main_test.go │ │ ├── power.go │ │ ├── sel.go │ │ ├── system.go │ │ ├── system_test.go │ │ ├── task.go │ │ ├── task_test.go │ │ ├── virtual_media.go │ │ └── virtual_media_test.go │ ├── sum/ │ │ ├── sum.go │ │ └── sum_test.go │ └── utils.go ├── lint.mk ├── logging/ │ └── logging.go ├── option.go └── providers/ ├── asrockrack/ │ ├── asrockrack.go │ ├── asrockrack_test.go │ ├── firmware.go │ ├── firmware_update.md │ ├── fixtures/ │ │ └── E3C246D4I-NL/ │ │ └── sensors.json │ ├── helpers.go │ ├── helpers_test.go │ ├── inventory.go │ ├── inventory_test.go │ ├── mock_test.go │ ├── power.go │ ├── user.go │ └── user_test.go ├── dell/ │ ├── firmware.go │ ├── firmware_test.go │ ├── fixtures/ │ │ ├── serviceroot.json │ │ ├── systems.json │ │ ├── systems_embedded.1.json │ │ ├── systems_embedded_no_manufacturer.1.json │ │ └── systems_embedded_not_dell.1.json │ ├── idrac.go │ └── idrac_test.go ├── homeassistant/ │ └── homeassistant.go ├── intelamt/ │ ├── intelamt.go │ └── intelamt_test.go ├── ipmitool/ │ ├── ipmitool.go │ └── ipmitool_test.go ├── openbmc/ │ ├── firmware.go │ └── openbmc.go ├── providers.go ├── redfish/ │ ├── fixtures/ │ │ └── v1/ │ │ ├── dell/ │ │ │ ├── entries.json │ │ │ ├── job_delete_ok.json │ │ │ ├── jobs.json │ │ │ ├── logservices.json │ │ │ ├── logservices.sel.json │ │ │ ├── manager.idrac.embedded.1.json │ │ │ ├── managers.json │ │ │ └── selentries/ │ │ │ ├── 1.json │ │ │ └── 2.json │ │ ├── serviceroot.json │ │ ├── systems.json │ │ └── updateservice.json │ ├── main_test.go │ ├── redfish.go │ ├── sel.go │ ├── sel_test.go │ └── user.go ├── rpc/ │ ├── doc.go │ ├── experimental.go │ ├── http.go │ ├── http_test.go │ ├── logging.go │ ├── payload.go │ ├── rpc.go │ ├── rpc_test.go │ └── signature.go └── supermicro/ ├── docs/ │ ├── x11.md │ └── x12.md ├── errors.go ├── firmware.go ├── firmware_bios_test.go ├── fixtures/ │ └── serviceroot.json ├── floppy.go ├── supermicro.go ├── supermicro_test.go ├── types.go ├── x11.go ├── x11_firmware_bios.go ├── x11_firmware_bmc.go ├── x11_firmware_bmc_test.go └── x12.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/Dockerfile ================================================ FROM mcr.microsoft.com/devcontainers/go:1-1.22-bullseye RUN apt update RUN apt install ipmitool -y ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/go { "name": "Go", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile // "image": "mcr.microsoft.com/devcontainers/go:1-1.21-bullseye", "build": { "dockerfile": "Dockerfile" }, // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "go version", // Configure tool-specific properties. "customizations": { "vscode": { "extensions": [ "ms-vscode.makefile-tools", "zxh404.vscode-proto3", "humao.rest-client" ] } } // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" } ================================================ FILE: .github/ISSUE_TEMPLATE/default.md ================================================ Please don't forget to include the following information in your issue: - The HW vendor impacted by this issue (if applicable) - The HW model number, BMC firmware and/or BIOS versions impacted by this issue (if applicable) - What version of bmclib exhibits this behavior (if applicable) - Detailed steps to verify it ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## What does this PR implement/change/remove? ### Checklist - [ ] Tests added - [ ] Similar commits squashed ### The HW vendor this change applies to (if applicable) ### The HW model number, product name this change applies to (if applicable) ### The BMC firmware and/or BIOS versions that this change applies to (if applicable) ### What version of tooling - vendor specific or opensource does this change depend on (if applicable) ## Description for changelog/release notes ``` ``` ================================================ FILE: .github/mergify.yml ================================================ queue_rules: - name: default batch_size: 1 queue_conditions: - base=main - "#approved-reviews-by>=1" - "#changes-requested-reviews-by=0" - check-success='lint' - check-success='test' - label!=do-not-merge - label=ready-to-merge merge_conditions: - base=main - "#approved-reviews-by>=1" - "#changes-requested-reviews-by=0" - check-success='lint' - check-success='test' - label!=do-not-merge - label=ready-to-merge merge_method: merge commit_message_template: | {{ title }} (#{{ number }}) pull_request_rules: - name: refactored queue action rule conditions: [] actions: queue: merge_queue: max_parallel_checks: 1 ================================================ FILE: .github/workflows/ci.yaml ================================================ name: For each commit and PR on: push: pull_request: jobs: lint: runs-on: ubuntu-latest env: CGO_ENABLED: 1 steps: - name: Checkout code uses: actions/checkout@v3 - name: Install Go uses: actions/setup-go@v3 with: go-version-file: go.mod - name: Run golangci-lint uses: golangci/golangci-lint-action@v3 with: args: -v --config .golangci.yml --timeout=5m version: latest - name: make all-checks run: make all-checks test: runs-on: ubuntu-latest env: CGO_ENABLED: 1 steps: - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 2 - name: Install Go uses: actions/setup-go@v3 with: go-version-file: go.mod - name: make all-tests run: make all-tests - name: upload codecov run: bash <(curl -s https://codecov.io/bash) ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Swap [._]*.s[a-v][a-z] [._]*.sw[a-p] [._]s[a-rt-v][a-z] [._]ss[a-gi-z] [._]sw[a-p] # Session Session.vim # Temporary .netrwhist *~ # Auto-generated tag files tags # Persistent undo [._]*.un~ # vscode *.code-workspace # added by lint-install out/ coverage.txt ================================================ FILE: .golangci.yml ================================================ govet: auto-fix: true linters-settings: enable: - fieldalignment check-shadowing: true settings: printf: funcs: - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf golint: min-confidence: 0 gocyclo: min-complexity: 10 maligned: suggest-new: true dupl: threshold: 100 goconst: min-len: 2 min-occurrences: 2 depguard: list-type: blacklist packages: # logging is allowed only by logutils.Log, logrus # is allowed to use only in logutils package - github.com/sirupsen/logrus misspell: locale: US auto-fix: true lll: line-length: 140 goimports: local-prefixes: github.com/golangci/golangci-lint gocritic: auto-fix: true enabled-tags: - performance - style - experimental disabled-checks: - wrapperFunc gofumpt: extra-rules: true auto-fix: true wsl: auto-fix: true stylecheck: auto-fix: true linters: enable: - errcheck - gosimple - govet - gofmt - gocyclo - ineffassign - stylecheck - deadcode - staticcheck - structcheck - unused - prealloc - typecheck - varcheck # additional linters - bodyclose - gocritic - whitespace - wsl - goimports - golint - misspell - goerr113 - noctx enable-all: false disable-all: true run: skip-dirs: issues: exclude-rules: - linters: - gosec text: "weak cryptographic primitive" - linters: - stylecheck text: "ST1016" exclude: # Default excludes from `golangci-lint run --help` with EXC0002 removed # EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked # EXC0002 golint: Annoying issue about not having a comment. The rare codebase has such comments # - (comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form) # EXC0003 golint: False positive when tests are defined in package 'test' - func name will be used as test\.Test.* by other packages, and that stutters; consider calling this # EXC0004 govet: Common false positives - (possible misuse of unsafe.Pointer|should have signature) # EXC0005 staticcheck: Developers tend to write in C-style with an explicit 'break' in a 'switch', so it's ok to ignore - ineffective break statement. Did you mean to break out of the outer loop # EXC0006 gosec: Too many false-positives on 'unsafe' usage - Use of unsafe calls should be audited # EXC0007 gosec: Too many false-positives for parametrized shell calls - Subprocess launch(ed with variable|ing should be audited) # EXC0008 gosec: Duplicated errcheck checks - (G104|G307) # EXC0009 gosec: Too many issues in popular repos - (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less) # EXC0010 gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)' - Potential file inclusion via variable exclude-use-default: false # golangci.com configuration # https://github.com/golangci/golangci/wiki/Configuration #service: # golangci-lint-version: 1.15.x # use the fixed version to not introduce new linters unexpectedly # prepare: # - echo "here I can run custom commands, but no preparation needed for this repo" ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [2022] [bmclib authors] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | cut -d":" -f2,3 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' .PHONY: test test: ## Run unit tests go test -v -covermode=atomic -race ./... .PHONY: cover cover: ## Run unit tests with coverage report go test -coverprofile=coverage.txt ./... go tool cover -func=coverage.txt .PHONY: all-tests all-tests: test cover ## run all tests .PHONY: all-checks all-checks: lint ## run all formatters go mod tidy go vet ./... -include lint.mk ================================================ FILE: README.md ================================================ # bmclib v2 - board management controller library [![Status](https://github.com/bmc-toolbox/bmclib/actions/workflows/ci.yaml/badge.svg)](https://github.com/bmc-toolbox/bmclib/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/bmc-toolbox/bmclib)](https://goreportcard.com/report/github.com/bmc-toolbox/bmclib/v2) [![GoDoc](https://godoc.org/github.com/bmc-toolbox/bmclib/v2?status.svg)](https://godoc.org/github.com/bmc-toolbox/bmclib/v2) bmclib v2 is a library to abstract interacting with baseboard management controllers. ## Supported BMC interfaces. - [Redfish](https://github.com/bmc-toolbox/bmclib/tree/main/providers/redfish) - [IPMItool](https://github.com/bmc-toolbox/bmclib/tree/main/providers/ipmitool) - [Intel AMT](https://github.com/bmc-toolbox/bmclib/tree/main/providers/intelamt) - [Asrockrack](https://github.com/bmc-toolbox/bmclib/tree/main/providers/asrockrack) - [RPC](providers/rpc/) ## Installation ```bash go get github.com/bmc-toolbox/bmclib/v2 ``` ## Import ```go import ( bmclib "github.com/bmc-toolbox/bmclib/v2" ) ``` ### Usage The snippet below connects to a BMC and retrieves the device hardware, firmware inventory. ```go import ( bmclib "github.com/bmc-toolbox/bmclib/v2" ) // setup logger l := logrus.New() l.Level = logrus.DebugLevel logger := logrusr.New(l) clientOpts := []bmclib.Option{bmclib.WithLogger(logger)} // init client client := bmclib.NewClient(*host, "admin", "hunter2", clientOpts...) // open BMC session err := client.Open(ctx) if err != nil { log.Fatal(err, "bmc login failed") } defer client.Close(ctx) // retrieve inventory data inventory, err := client.Inventory(ctx) if err != nil { l.Error(err) } b, err := json.MarshalIndent(inventory, "", " ") if err != nil { l.Error(err) } fmt.Println(string(b)) ``` More sample code can be found in [examples](./examples/) ## BMC connections bmclib performs queries on BMCs using [multiple `drivers`](https://github.com/bmc-toolbox/bmclib/blob/main/bmc/connection.go#L30), these `drivers` are the various services exposed by a BMC - `redfish` `IPMI` `SSH` and `vendor API` which is basically a custom vendor API endpoint. The bmclib client determines which driver to use for an action like `Power cycle` or `Create user` based on its availability or through a compatibility test (when enabled). When querying multiple BMCs through bmclib its often useful to to limit the BMCs and drivers that bmclib will attempt to use to connect, the options to limit or filter out BMCs are described below, Query just using the `redfish` endpoint. ```go cl := bmclib.NewClient("192.168.1.1", "admin", "hunter2") cl.Registry.Drivers = cl.Registry.Using("redfish") ``` Query using the `redfish` endpoint and fall back to `IPMI` ```go client := bmclib.NewClient("192.168.1.1", "admin", "hunter2") // overwrite registered drivers by appending Redfish, IPMI drivers in order drivers := append(registrar.Drivers{}, bmcClient.Registry.Using("redfish")...) drivers = append(drivers, bmcClient.Registry.Using("ipmi")...) client.Registry.Drivers = driver ``` Filter drivers to query based on compatibility, this will attempt to check if the driver is [compatible](https://github.com/bmc-toolbox/bmclib/blob/main/providers/redfish/redfish.go#L70) ideally, this method should be invoked when the client is ready to perform a BMC action. ```go client := bmclib.NewClient("192.168.1.1", "admin", "hunter2") client.Registry.Drivers = cl.Registry.FilterForCompatible(ctx) ``` Ignore the Redfish endpoint completely on BMCs running a specific Redfish version. Note: this version should match the one returned through `curl -k "https:///redfish/v1" | jq .RedfishVersion` ```go opt := bmclib.WithRedfishVersionsNotCompatible([]string{"1.5.0"}) client := bmclib.NewClient("192.168.1.1", "admin", "hunter2", opt...) cl.Registry.Drivers = cl.Registry.FilterForCompatible(ctx) ``` ## Timeouts bmclib can be configured to apply timeouts to BMC interactions. The following options are available. **Total max timeout only** - The total time bmclib will wait for all BMC interactions to complete. This is specified using a single `context.WithTimeout` or `context.WithDeadline` that is passed to all method call. With this option, the per provider; per interaction timeout is calculated by the total max timeout divided by the number of providers (currently there are 4 providers). ```go cl := bmclib.NewClient(host, user, pass, bmclib.WithLogger(log)) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err = cl.Open(ctx); err != nil { return(err) } defer cl.Close(ctx) state, err := cl.GetPowerState(ctx) ``` **Total max timeout and a per provider; per interaction timeout** - The total time bmclib will wait for all BMC interactions to complete. This is specified using a single `context.WithTimeout` or `context.WithDeadline` that is passed to all method call. This is honored above all timeouts. The per provider; per interaction timeout is specified using `bmclib.WithPerProviderTimeout` in the Client constructor. ```go cl := bmclib.NewClient(host, user, pass, bmclib.WithLogger(log), bmclib.WithPerProviderTimeout(15*time.Second)) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() if err = cl.Open(ctx); err != nil { return(err) } defer cl.Close(ctx) state, err := cl.GetPowerState(ctx) ``` **Per provider; per interaction timeout. No total max timeout** - The time bmclib will wait for a specific provider to complete. This is specified using `bmclib.WithPerProviderTimeout` in the Client constructor. ```go cl := bmclib.NewClient(host, user, pass, bmclib.WithLogger(log), bmclib.WithPerProviderTimeout(15*time.Second)) ctx := context.Background() if err = cl.Open(ctx); err != nil { return(err) } defer cl.Close(ctx) state, err := cl.GetPowerState(ctx) ``` **Default timeout** - If no timeout is specified with a context or with `bmclib.WithPerProviderTimeout` the default is used. 30 seconds per provider; per interaction. ```go cl := bmclib.NewClient(host, user, pass, bmclib.WithLogger(log)) ctx := context.Background() if err = cl.Open(ctx); err != nil { return(err) } defer cl.Close(ctx) state, err := cl.GetPowerState(ctx) ``` ## Filtering The `bmclib.Client` can be configured to filter BMC calls based on a few different criteria. Filtering modifies the order and/or the number of providers for BMC calls. This filtering can be permanent or on a one-time basis. All providers are stored in a registry (see [`Client.Registry`](https://github.com/bmc-toolbox/bmclib/blob/b5cdfa3ffe026d3cc3257953abe3234b278ca20a/client.go#L29)) and the default order for providers in the registry is `ipmitool`, `asrockrack`, `gofish`, `IntelAMT`. The default order is defined [here](https://github.com/bmc-toolbox/bmclib/blob/b5cdfa3ffe026d3cc3257953abe3234b278ca20a/client.go#L152). ### Permanent Filtering Permanent filtering modifies the order and/or the number of providers for BMC calls for all client methods (for example: `Open`, `SetPowerState`, etc) calls. ```go cl := bmclib.NewClient(host, user, pass) // This will modify the order for all subsequent BMC calls cl.Registry.Drivers = cl.Registry.PreferDriver("gofish") if err := cl.Open(ctx); err != nil { return(err) } ``` The following permanent filters are available: - `cl.Registry.PreferDriver("gofish")` - This moves the `gofish` provider to be the first provider in the registry. - `cl.Registry.Supports(providers.FeaturePowerSet)` - This removes any provider from the registry that does not support the setting the power state. - `cl.Registry.Using("redfish")` - This removes any provider from the registry that does not support the `redfish` protocol. - `cl.Registry.For("gofish")` - This removes any provider from the registry that is not the `gofish` provider. - `cl.Registry.PreferProtocol("redfish")` - This moves any provider that implements the `redfish` protocol to the beginning of the registry. ### One-time Filtering One-time filtering modifies the order and/or the number of providers for BMC calls only for a single method call. ```Go cl := bmclib.NewClient(host, user, pass) // This will modify the order for only this BMC call if err := cl.PreferProvider("gofish").Open(ctx); err != nil { return(err) } ``` The following one-time filters are available: - `cl.PreferProtocol("gofish").GetPowerState(ctx)` - This moves the `gofish` provider to be the first provider in the registry. - `cl.Supports(providers.FeaturePowerSet).GetPowerState(ctx)` - This removes any provider from the registry that does not support the setting the power state. - `cl.Using("redfish").GetPowerState(ctx)` - This removes any provider from the registry that does not support the `redfish` protocol. - `cl.For("gofish").GetPowerState(ctx)` - This removes any provider from the registry that is not the `gofish` provider. - `cl.PreferProtocol("redfish").GetPowerState(ctx)` - This moves any provider that implements the `redfish` protocol to the beginning of the registry. ### Tracing To collect trace telemetry, set the `WithTraceProvider()` option on the client which results in trace spans being collected for each client method. ```go cl := bmclib.NewClient( host, user, pass, bmclib.WithLogger(log), bmclib.WithTracerProvider(otel.GetTracerProvider()), ) ``` ## Versions The current bmclib version is `v2` and is being developed on the `main` branch. The previous bmclib version is in maintenance mode and can be found here [v1](https://github.com/bmc-toolbox/bmclib/v1). ## Go version in `go.mod` As a library we will only bump the version of Go in the `go.mod` file when there are required dependencies in bmclib that necessitate a version bump. When consuming bmclib in your project, we recommend always building with the latest Go version but this should be in your hands as a user as much as possible. ## Acknowledgments bmclib v2 interfaces with Redfish on BMCs through the Gofish library https://github.com/stmcginnis/gofish bmclib was originally developed for [Booking.com](http://www.booking.com). With approval from [Booking.com](http://www.booking.com), the code and specification were generalized and published as Open Source on github, for which the authors would like to express their gratitude. ### Authors - [Joel Rebello](https://github.com/joelrebel) - [Jacob Weinstock](https://github.com/jacobweinstock) ================================================ FILE: bmc/bios.go ================================================ package bmc import ( "context" "fmt" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) type BiosConfigurationGetter interface { GetBiosConfiguration(ctx context.Context) (biosConfig map[string]string, err error) } type biosConfigurationGetterProvider struct { name string BiosConfigurationGetter } type BiosConfigurationSetter interface { SetBiosConfiguration(ctx context.Context, biosConfig map[string]string) (err error) SetBiosConfigurationFromFile(ctx context.Context, cfg string) (err error) } type biosConfigurationSetterProvider struct { name string BiosConfigurationSetter } type BiosConfigurationResetter interface { ResetBiosConfiguration(ctx context.Context) (err error) } type biosConfigurationResetterProvider struct { name string BiosConfigurationResetter } func biosConfiguration(ctx context.Context, generic []biosConfigurationGetterProvider) (biosConfig map[string]string, metadata Metadata, err error) { metadata = newMetadata() Loop: for _, elem := range generic { if elem.BiosConfigurationGetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) break Loop default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) biosConfig, vErr := elem.GetBiosConfiguration(ctx) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) continue } metadata.SuccessfulProvider = elem.name return biosConfig, metadata, nil } } return biosConfig, metadata, multierror.Append(err, errors.New("failure to get bios configuration")) } func setBiosConfiguration(ctx context.Context, generic []biosConfigurationSetterProvider, biosConfig map[string]string) (metadata Metadata, err error) { metadata = newMetadata() Loop: for _, elem := range generic { if elem.BiosConfigurationSetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) break Loop default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) vErr := elem.SetBiosConfiguration(ctx, biosConfig) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) continue } metadata.SuccessfulProvider = elem.name return metadata, nil } } return metadata, multierror.Append(err, errors.New("failure to set bios configuration")) } func setBiosConfigurationFromFile(ctx context.Context, generic []biosConfigurationSetterProvider, cfg string) (metadata Metadata, err error) { metadata = newMetadata() Loop: for _, elem := range generic { if elem.BiosConfigurationSetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) break Loop default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) vErr := elem.SetBiosConfigurationFromFile(ctx, cfg) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) continue } metadata.SuccessfulProvider = elem.name return metadata, nil } } return metadata, multierror.Append(err, errors.New("failure to set bios configuration from file")) } func resetBiosConfiguration(ctx context.Context, generic []biosConfigurationResetterProvider) (metadata Metadata, err error) { metadata = newMetadata() Loop: for _, elem := range generic { if elem.BiosConfigurationResetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) break Loop default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) vErr := elem.ResetBiosConfiguration(ctx) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) continue } metadata.SuccessfulProvider = elem.name return metadata, nil } } return metadata, multierror.Append(err, errors.New("failure to reset bios configuration")) } func GetBiosConfigurationInterfaces(ctx context.Context, generic []interface{}) (biosConfig map[string]string, metadata Metadata, err error) { implementations := make([]biosConfigurationGetterProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := biosConfigurationGetterProvider{name: getProviderName(elem)} switch p := elem.(type) { case BiosConfigurationGetter: temp.BiosConfigurationGetter = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a BiosConfigurationGetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return biosConfig, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no BiosConfigurationGetter implementations found"), ), ) } return biosConfiguration(ctx, implementations) } func SetBiosConfigurationInterfaces(ctx context.Context, generic []interface{}, biosConfig map[string]string) (metadata Metadata, err error) { implementations := make([]biosConfigurationSetterProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := biosConfigurationSetterProvider{name: getProviderName(elem)} switch p := elem.(type) { case BiosConfigurationSetter: temp.BiosConfigurationSetter = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a BiosConfigurationSetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no BiosConfigurationSetter implementations found"), ), ) } return setBiosConfiguration(ctx, implementations, biosConfig) } func SetBiosConfigurationFromFileInterfaces(ctx context.Context, generic []interface{}, cfg string) (metadata Metadata, err error) { implementations := make([]biosConfigurationSetterProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := biosConfigurationSetterProvider{name: getProviderName(elem)} switch p := elem.(type) { case BiosConfigurationSetter: temp.BiosConfigurationSetter = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a BiosConfigurationSetterFromFile implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no BiosConfigurationSetterFromFile implementations found"), ), ) } return setBiosConfigurationFromFile(ctx, implementations, cfg) } func ResetBiosConfigurationInterfaces(ctx context.Context, generic []interface{}) (metadata Metadata, err error) { implementations := make([]biosConfigurationResetterProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := biosConfigurationResetterProvider{name: getProviderName(elem)} switch p := elem.(type) { case BiosConfigurationResetter: temp.BiosConfigurationResetter = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a BiosConfigurationResetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no BiosConfigurationResetter implementations found"), ), ) } return resetBiosConfiguration(ctx, implementations) } ================================================ FILE: bmc/bmc.go ================================================ package bmc import ( "strings" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) // Metadata represents details about a bmc method type Metadata struct { // SuccessfulProvider is the name of the provider that successfully executed SuccessfulProvider string // ProvidersAttempted is a slice of all providers that were attempt to execute ProvidersAttempted []string // SuccessfulOpenConns is a slice of provider names that were opened successfully SuccessfulOpenConns []string // SuccessfulCloseConns is a slice of provider names that were closed successfully SuccessfulCloseConns []string // FailedProviderDetail holds the failed providers error messages for called methods FailedProviderDetail map[string]string } func newMetadata() Metadata { return Metadata{ FailedProviderDetail: make(map[string]string), } } func (m *Metadata) RegisterSpanAttributes(host string, span trace.Span) { span.SetAttributes(attribute.String("host", host)) span.SetAttributes(attribute.String("successful-provider", m.SuccessfulProvider)) span.SetAttributes( attribute.String("successful-open-conns", strings.Join(m.SuccessfulOpenConns, ",")), ) span.SetAttributes( attribute.String("successful-close-conns", strings.Join(m.SuccessfulCloseConns, ",")), ) span.SetAttributes( attribute.String("attempted-providers", strings.Join(m.ProvidersAttempted, ",")), ) for p, e := range m.FailedProviderDetail { span.SetAttributes( attribute.String("provider-errs-"+p, e), ) } } ================================================ FILE: bmc/boot_device.go ================================================ package bmc import ( "context" "fmt" "time" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) type BootDeviceType string const ( BootDeviceTypeBIOS BootDeviceType = "bios" BootDeviceTypeCDROM BootDeviceType = "cdrom" BootDeviceTypeDiag BootDeviceType = "diag" BootDeviceTypeFloppy BootDeviceType = "floppy" BootDeviceTypeDisk BootDeviceType = "disk" BootDeviceTypeNone BootDeviceType = "none" BootDeviceTypePXE BootDeviceType = "pxe" BootDeviceTypeRemoteDrive BootDeviceType = "remote_drive" BootDeviceTypeSDCard BootDeviceType = "sd_card" BootDeviceTypeUSB BootDeviceType = "usb" BootDeviceTypeUtil BootDeviceType = "utilities" BootDeviceUefiHTTP BootDeviceType = "uefi_http" ) // BootDeviceSetter sets the next boot device for a machine type BootDeviceSetter interface { BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) } // BootDeviceOverrideGetter gets boot override settings for a machine type BootDeviceOverrideGetter interface { BootDeviceOverrideGet(ctx context.Context) (override BootDeviceOverride, err error) } // bootDeviceProviders is an internal struct to correlate an implementation/provider and its name type bootDeviceProviders struct { name string bootDeviceSetter BootDeviceSetter } // bootOverrideProvider is an internal struct to correlate an implementation/provider and its name type bootOverrideProvider struct { name string bootOverrideGetter BootDeviceOverrideGetter } type BootDeviceOverride struct { IsPersistent bool IsEFIBoot bool Device BootDeviceType } // setBootDevice sets the next boot device. // // setPersistent persists the next boot device. // efiBoot sets up the device to boot off UEFI instead of legacy. func setBootDevice(ctx context.Context, timeout time.Duration, bootDevice string, setPersistent, efiBoot bool, b []bootDeviceProviders) (ok bool, metadata Metadata, err error) { metadataLocal := newMetadata() for _, elem := range b { if elem.bootDeviceSetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return false, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ok, setErr := elem.bootDeviceSetter.BootDeviceSet(ctx, bootDevice, setPersistent, efiBoot) if setErr != nil { err = multierror.Append(err, errors.WithMessagef(setErr, "provider: %v", elem.name)) metadataLocal.FailedProviderDetail[elem.name] = setErr.Error() continue } if !ok { err = multierror.Append(err, fmt.Errorf("provider: %v, failed to set boot device", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return ok, metadataLocal, nil } } return ok, metadataLocal, multierror.Append(err, errors.New("failed to set boot device")) } // SetBootDeviceFromInterfaces identifies implementations of the BootDeviceSetter interface and passes the found implementations to the setBootDevice() wrapper func SetBootDeviceFromInterfaces(ctx context.Context, timeout time.Duration, bootDevice string, setPersistent, efiBoot bool, generic []interface{}) (ok bool, metadata Metadata, err error) { bdSetters := make([]bootDeviceProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := bootDeviceProviders{name: getProviderName(elem)} switch p := elem.(type) { case BootDeviceSetter: temp.bootDeviceSetter = p bdSetters = append(bdSetters, temp) default: e := fmt.Sprintf("not a BootDeviceSetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(bdSetters) == 0 { return ok, metadata, multierror.Append(err, errors.New("no BootDeviceSetter implementations found")) } return setBootDevice(ctx, timeout, bootDevice, setPersistent, efiBoot, bdSetters) } // getBootDeviceOverride gets the boot device override settings for the given provider, // and updates the given metadata with provider attempts and errors. func getBootDeviceOverride( ctx context.Context, timeout time.Duration, provider *bootOverrideProvider, metadata *Metadata, ) (override BootDeviceOverride, ok bool, err error) { select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return override, ok, err default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, provider.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() override, err = provider.bootOverrideGetter.BootDeviceOverrideGet(ctx) if err != nil { metadata.FailedProviderDetail[provider.name] = err.Error() return override, ok, nil } metadata.SuccessfulProvider = provider.name return override, true, nil } } // GetBootDeviceOverrideFromInterface will get boot device override settings from the first successful // call to a BootDeviceOverrideGetter in the array of providers. func GetBootDeviceOverrideFromInterface( ctx context.Context, timeout time.Duration, providers []interface{}, ) (override BootDeviceOverride, metadata Metadata, err error) { metadata = newMetadata() for _, elem := range providers { if elem == nil { continue } switch p := elem.(type) { case BootDeviceOverrideGetter: provider := &bootOverrideProvider{name: getProviderName(elem), bootOverrideGetter: p} override, ok, getErr := getBootDeviceOverride(ctx, timeout, provider, &metadata) if getErr != nil || ok { return override, metadata, getErr } default: e := fmt.Errorf("not a BootDeviceOverrideGetter implementation: %T", p) err = multierror.Append(err, e) } } if len(metadata.ProvidersAttempted) == 0 { err = multierror.Append(err, errors.New("no BootDeviceOverrideGetter implementations found")) } else { err = multierror.Append(err, errors.New("failed to get boot device override settings")) } return override, metadata, err } ================================================ FILE: bmc/boot_device_test.go ================================================ package bmc import ( "context" "errors" "testing" "time" "fmt" "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-multierror" "github.com/stretchr/testify/assert" ) type bootDeviceTester struct { MakeNotOK bool MakeErrorOut bool } func (b *bootDeviceTester) BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { if b.MakeErrorOut { return ok, errors.New("boot device set failed") } if b.MakeNotOK { return false, nil } return true, nil } func (b *bootDeviceTester) Name() string { return "test provider" } func TestSetBootDevice(t *testing.T) { testCases := map[string]struct { bootDevice string makeErrorOut bool makeNotOk bool want bool err error ctxTimeout time.Duration }{ "success": {bootDevice: "pxe", want: true}, "not ok return": {bootDevice: "pxe", want: false, makeNotOk: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider, failed to set boot device"), errors.New("failed to set boot device")}}}, "error": {bootDevice: "pxe", want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider: boot device set failed"), errors.New("failed to set boot device")}}}, "error context timeout": {bootDevice: "pxe", want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := bootDeviceTester{MakeErrorOut: tc.makeErrorOut, MakeNotOK: tc.makeNotOk} expectedResult := tc.want if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() result, _, err := setBootDevice(ctx, 0, tc.bootDevice, false, false, []bootDeviceProviders{{"test provider", &testImplementation}}) if err != nil { if tc.err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } }) } } func TestSetBootDeviceFromInterfaces(t *testing.T) { testCases := map[string]struct { bootDevice string err error badImplementation bool want bool withName bool }{ "success": {bootDevice: "pxe", want: true}, "success with metadata": {bootDevice: "pxe", want: true, withName: true}, "no implementations found": {bootDevice: "pxe", want: false, badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a BootDeviceSetter implementation: *struct {}"), errors.New("no BootDeviceSetter implementations found")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := bootDeviceTester{} generic = []interface{}{&testImplementation} } expectedResult := tc.want result, metadata, err := SetBootDeviceFromInterfaces(context.Background(), 0, tc.bootDevice, false, false, generic) if err != nil { diff := cmp.Diff(tc.err.Error(), err.Error()) if diff != "" { t.Fatal(diff) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } if tc.withName { if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { t.Fatal(diff) } } }) } } type mockBootDeviceOverrideGetter struct { overrideReturn BootDeviceOverride errReturn error } func (m *mockBootDeviceOverrideGetter) Name() string { return "Mock" } func (m *mockBootDeviceOverrideGetter) BootDeviceOverrideGet(_ context.Context) (BootDeviceOverride, error) { return m.overrideReturn, m.errReturn } func TestBootDeviceOverrideGet(t *testing.T) { successOverride := BootDeviceOverride{ IsPersistent: false, IsEFIBoot: true, Device: BootDeviceTypeDisk, } successMetadata := &Metadata{ SuccessfulProvider: "Mock", ProvidersAttempted: []string{"Mock"}, SuccessfulOpenConns: nil, SuccessfulCloseConns: []string(nil), FailedProviderDetail: map[string]string{}, } mixedMetadata := &Metadata{ SuccessfulProvider: "Mock", ProvidersAttempted: []string{"Mock", "Mock"}, SuccessfulOpenConns: nil, SuccessfulCloseConns: []string(nil), FailedProviderDetail: map[string]string{"Mock": "foo-failure"}, } failMetadata := &Metadata{ SuccessfulProvider: "", ProvidersAttempted: []string{"Mock"}, SuccessfulOpenConns: nil, SuccessfulCloseConns: []string(nil), FailedProviderDetail: map[string]string{"Mock": "foo-failure"}, } emptyMetadata := &Metadata{ FailedProviderDetail: make(map[string]string), } testCases := []struct { name string hasCanceledContext bool expectedErrorMsg string expectedMetadata *Metadata expectedOverride BootDeviceOverride getters []interface{} }{ { name: "success", expectedMetadata: successMetadata, expectedOverride: successOverride, getters: []interface{}{ &mockBootDeviceOverrideGetter{overrideReturn: successOverride}, }, }, { name: "multiple getters", expectedMetadata: mixedMetadata, expectedOverride: successOverride, getters: []interface{}{ "not a getter", &mockBootDeviceOverrideGetter{errReturn: fmt.Errorf("foo-failure")}, &mockBootDeviceOverrideGetter{overrideReturn: successOverride}, }, }, { name: "error", expectedMetadata: failMetadata, expectedErrorMsg: "failed to get boot device override settings", getters: []interface{}{ &mockBootDeviceOverrideGetter{errReturn: fmt.Errorf("foo-failure")}, }, }, { name: "nil BootDeviceOverrideGetters", expectedMetadata: emptyMetadata, expectedErrorMsg: "no BootDeviceOverrideGetter implementations found", }, { name: "nil BootDeviceOverrideGetter", expectedMetadata: emptyMetadata, expectedErrorMsg: "no BootDeviceOverrideGetter implementations found", getters: []interface{}{nil}, }, { name: "with canceled context", hasCanceledContext: true, expectedMetadata: emptyMetadata, expectedErrorMsg: "context canceled", getters: []interface{}{ &mockBootDeviceOverrideGetter{}, }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() if testCase.hasCanceledContext { cancel() } override, metadata, err := GetBootDeviceOverrideFromInterface(ctx, 0, testCase.getters) if testCase.expectedErrorMsg != "" { assert.ErrorContains(t, err, testCase.expectedErrorMsg) } else { assert.Nil(t, err) } assert.Equal(t, testCase.expectedOverride, override) assert.Equal(t, testCase.expectedMetadata, &metadata) }) } } ================================================ FILE: bmc/connection.go ================================================ package bmc import ( "context" "fmt" "sync" "time" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) // Opener interface for opening a connection to a BMC type Opener interface { Open(ctx context.Context) error } // Closer interface for closing a connection to a BMC type Closer interface { Close(ctx context.Context) error } // connectionProviders is an internal struct to correlate an implementation/provider and its name type connectionProviders struct { name string closer Closer } // OpenConnectionFromInterfaces will try all opener interfaces and remove failed ones. // The reason failed ones need to be removed is so that when other methods are called (like powerstate) // implementations that have connections wont nil pointer error when their connection fails. func OpenConnectionFromInterfaces(ctx context.Context, timeout time.Duration, providers []interface{}) (opened []interface{}, metadata Metadata, err error) { metadata = newMetadata() // Return immediately if the context is done. select { case <-ctx.Done(): return nil, metadata, multierror.Append(err, ctx.Err()) default: } // Create a context with the specified timeout. This is done for backward compatibility but // we should consider removing the timeout parameter alltogether given the context will // container the timeout. ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() // result facilitates communication of data between the concurrent opener goroutines and // the the parent goroutine. type result struct { ProviderName string Opener Opener Err error } // Create a channel to communicate results between opener goroutines and the parent goroutine. results := make(chan result) // Use a WaitGroup to control closing of the results channel when all opener goroutines finish. var wg sync.WaitGroup // For every provider, launch a goroutine that attempts to open a connection and report // back via the results channel what happened. for _, elem := range providers { if elem == nil { continue } switch p := elem.(type) { case Opener: providerName := getProviderName(elem) metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, providerName) wg.Add(1) go func(provider Opener, providerName string) { defer wg.Done() res := result{ProviderName: providerName, Opener: provider} if err := provider.Open(ctx); err != nil { res.Err = errors.WithMessagef(err, "provider: %v", providerName) } results <- res }(p, providerName) default: err = multierror.Append(err, fmt.Errorf("not a Opener implementation: %T", p)) } } // Launch the goroutine to close the results channel ensuring we can exit the for-range over // the results channel below. go func() { wg.Wait(); close(results) }() // Gather and handle results from the opener goroutines. for res := range results { if res.Err != nil { err = multierror.Append(err, res.Err) metadata.FailedProviderDetail[res.ProviderName] = res.Err.Error() continue } opened = append(opened, res.Opener) metadata.SuccessfulOpenConns = append(metadata.SuccessfulOpenConns, res.ProviderName) } if len(opened) == 0 { return nil, metadata, multierror.Append(err, errors.New("no Opener implementations found")) } return opened, metadata, nil } // closeConnection closes a connection to a BMC, trying all interface implementations passed in func closeConnection(ctx context.Context, c []connectionProviders) (metadata Metadata, err error) { metadata = newMetadata() var connClosed bool for _, elem := range c { if elem.closer == nil { continue } metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) closeErr := elem.closer.Close(ctx) if closeErr != nil { err = multierror.Append(err, errors.WithMessagef(closeErr, "provider: %v", elem.name)) metadata.FailedProviderDetail[elem.name] = closeErr.Error() continue } connClosed = true metadata.SuccessfulCloseConns = append(metadata.SuccessfulCloseConns, elem.name) } if connClosed { return metadata, nil } return metadata, multierror.Append(err, errors.New("failed to close connection")) } // CloseConnectionFromInterfaces identifies implementations of the Closer() interface and and passes the found implementations to the closeConnection() wrapper func CloseConnectionFromInterfaces(ctx context.Context, generic []interface{}) (metadata Metadata, err error) { metadata = newMetadata() closers := make([]connectionProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := connectionProviders{name: getProviderName(elem)} switch p := elem.(type) { case Closer: temp.closer = p closers = append(closers, temp) default: e := fmt.Sprintf("not a Closer implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(closers) == 0 { return metadata, multierror.Append(err, errors.New("no Closer implementations found")) } return closeConnection(ctx, closers) } ================================================ FILE: bmc/connection_test.go ================================================ package bmc import ( "context" "errors" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-multierror" "go.uber.org/goleak" ) type connTester1 struct { MakeErrorOut bool } func (r *connTester1) Open(ctx context.Context) (err error) { if r.MakeErrorOut { return errors.New("open connection failed") } return nil } func (r *connTester1) Close(ctx context.Context) (err error) { if r.MakeErrorOut { return errors.New("close connection failed") } return nil } func (p *connTester1) Name() string { return "test provider" } type connTester2 struct{} func (r *connTester2) Open(ctx context.Context) (err error) { <-ctx.Done() return nil } func (r *connTester2) Close(ctx context.Context) (err error) { return nil } func (p *connTester2) Name() string { return "test provider 2" } func TestOpenConnectionFromInterfaces(t *testing.T) { testCases := map[string]struct { err error makeErrorOut bool badImplementation bool withMetadata bool withMultipleProviders bool ctxTimeout time.Duration }{ "success": {}, "success with metadata": {withMetadata: true}, "error context deadline": {err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded"), errors.New("no Opener implementations found")}}, ctxTimeout: time.Nanosecond * 1}, "error failed open": {makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider: open connection failed"), errors.New("no Opener implementations found")}}}, "no implementations found": {badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a Opener implementation: *struct {}"), errors.New("no Opener implementations found")}}}, "multiple providers attempted": {withMultipleProviders: true}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { defer goleak.VerifyNone(t) var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = append(generic, &badImplementation) } else { testImplementation := &connTester1{MakeErrorOut: tc.makeErrorOut} generic = append(generic, testImplementation) } if tc.withMultipleProviders { generic = append(generic, &connTester2{}) } if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx := context.Background() opened, metadata, err := OpenConnectionFromInterfaces(ctx, tc.ctxTimeout, generic) if err != nil { if tc.err != nil { if diff := cmp.Diff(err.Error(), tc.err.Error()); diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else { expected := []interface{}{&connTester1{}} if tc.withMultipleProviders { expected = append(expected, &connTester2{}) } if diff := cmp.Diff(opened, expected); diff != "" { t.Fatal(diff) } } if tc.withMetadata { if diff := cmp.Diff(metadata.SuccessfulOpenConns, []string{"test provider"}); diff != "" { t.Fatal(diff) } } }) } } func TestCloseConnection(t *testing.T) { testCases := map[string]struct { makeErrorOut bool err error ctxTimeout time.Duration }{ "success": {}, "error context deadline": {err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, "error": {makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider: close connection failed"), errors.New("failed to close connection")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := connTester1{MakeErrorOut: tc.makeErrorOut} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() _, err := closeConnection(ctx, []connectionProviders{{"test provider", &testImplementation}}) if err != nil { diff := cmp.Diff(tc.err.Error(), err.Error()) if diff != "" { t.Fatal(diff) } } }) } } func TestCloseConnectionFromInterfaces(t *testing.T) { testCases := map[string]struct { err error badImplementation bool withMetadata bool }{ "success": {}, "success with metadata": {withMetadata: true}, "no implementations found": {badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a Closer implementation: *struct {}"), errors.New("no Closer implementations found")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := connTester1{} generic = []interface{}{&testImplementation} } metadata, err := CloseConnectionFromInterfaces(context.Background(), generic) if err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } if tc.withMetadata { if diff := cmp.Diff(metadata.SuccessfulCloseConns, []string{"test provider"}); diff != "" { t.Fatal(diff) } } }) } } ================================================ FILE: bmc/firmware.go ================================================ package bmc import ( "context" "fmt" "io" "os" "github.com/bmc-toolbox/bmclib/v2/constants" bconsts "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) // FirmwareInstaller defines an interface to upload and initiate a firmware install type FirmwareInstaller interface { // FirmwareInstall uploads firmware update payload to the BMC returning the task ID // // parameters: // component - the component slug for the component update being installed. // operationsApplyTime - one of the OperationApplyTime constants // forceInstall - purge the install task queued/scheduled firmware install BMC task (if any). // reader - the io.reader to the firmware update file. // // return values: // taskID - A taskID is returned if the update process on the BMC returns an identifier for the update process. FirmwareInstall(ctx context.Context, component string, operationApplyTime string, forceInstall bool, reader io.Reader) (taskID string, err error) } // firmwareInstallerProvider is an internal struct to correlate an implementation/provider and its name type firmwareInstallerProvider struct { name string FirmwareInstaller } // firmwareInstall uploads and initiates firmware update for the component func firmwareInstall(ctx context.Context, component, operationApplyTime string, forceInstall bool, reader io.Reader, generic []firmwareInstallerProvider) (taskID string, metadata Metadata, err error) { metadata = newMetadata() for _, elem := range generic { if elem.FirmwareInstaller == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return taskID, metadata, err default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) taskID, vErr := elem.FirmwareInstall(ctx, component, operationApplyTime, forceInstall, reader) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) metadata.FailedProviderDetail[elem.name] = err.Error() continue } metadata.SuccessfulProvider = elem.name return taskID, metadata, nil } } return taskID, metadata, multierror.Append(err, errors.New("failure in FirmwareInstall")) } // FirmwareInstallFromInterfaces identifies implementations of the FirmwareInstaller interface and passes the found implementations to the firmwareInstall() wrapper func FirmwareInstallFromInterfaces(ctx context.Context, component, operationApplyTime string, forceInstall bool, reader io.Reader, generic []interface{}) (taskID string, metadata Metadata, err error) { metadata = newMetadata() implementations := make([]firmwareInstallerProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := firmwareInstallerProvider{name: getProviderName(elem)} switch p := elem.(type) { case FirmwareInstaller: temp.FirmwareInstaller = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a FirmwareInstaller implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return taskID, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no FirmwareInstaller implementations found"), ), ) } return firmwareInstall(ctx, component, operationApplyTime, forceInstall, reader, implementations) } // Note: this interface is to be deprecated in favour of a more generic FirmwareTaskVerifier. // // FirmwareInstallVerifier defines an interface to check firmware install status type FirmwareInstallVerifier interface { // FirmwareInstallStatus returns the status of the firmware install process. // // parameters: // installVersion (required) - the version this method should check is installed. // component (optional) - the component slug for the component update being installed. // taskID (optional) - the task identifier. // // return values: // status - returns one of the FirmwareInstall statuses (see devices/constants.go). FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (status string, err error) } // firmwareInstallVerifierProvider is an internal struct to correlate an implementation/provider and its name type firmwareInstallVerifierProvider struct { name string FirmwareInstallVerifier } // firmwareInstallStatus returns the status of the firmware install process func firmwareInstallStatus(ctx context.Context, installVersion, component, taskID string, generic []firmwareInstallVerifierProvider) (status string, metadata Metadata, err error) { metadata = newMetadata() for _, elem := range generic { if elem.FirmwareInstallVerifier == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return status, metadata, err default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) status, vErr := elem.FirmwareInstallStatus(ctx, installVersion, component, taskID) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) metadata.FailedProviderDetail[elem.name] = err.Error() continue } metadata.SuccessfulProvider = elem.name return status, metadata, nil } } return status, metadata, multierror.Append(err, errors.New("failure in FirmwareInstallStatus")) } // FirmwareInstallStatusFromInterfaces identifies implementations of the FirmwareInstallVerifier interface and passes the found implementations to the firmwareInstallStatus() wrapper. func FirmwareInstallStatusFromInterfaces(ctx context.Context, installVersion, component, taskID string, generic []interface{}) (status string, metadata Metadata, err error) { metadata = newMetadata() implementations := make([]firmwareInstallVerifierProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := firmwareInstallVerifierProvider{name: getProviderName(elem)} switch p := elem.(type) { case FirmwareInstallVerifier: temp.FirmwareInstallVerifier = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a FirmwareInstallVerifier implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return taskID, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no FirmwareInstallVerifier implementations found"), ), ) } return firmwareInstallStatus(ctx, installVersion, component, taskID, implementations) } // FirmwareInstallProvider defines an interface to upload and initiate a firmware install in the same implementation method // // Its intended to deprecate the FirmwareInstall interface type FirmwareInstallProvider interface { // FirmwareInstallUploadAndInitiate uploads _and_ initiates the firmware install process. // // return values: // taskID - A taskID is returned if the update process on the BMC returns an identifier for the update process. FirmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File) (taskID string, err error) } // firmwareInstallProvider is an internal struct to correlate an implementation/provider and its name type firmwareInstallProvider struct { name string FirmwareInstallProvider } // firmwareInstall uploads and initiates firmware update for the component func firmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File, generic []firmwareInstallProvider) (taskID string, metadata Metadata, err error) { metadata = newMetadata() for _, elem := range generic { if elem.FirmwareInstallProvider == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return taskID, metadata, err default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) taskID, vErr := elem.FirmwareInstallUploadAndInitiate(ctx, component, file) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) metadata.FailedProviderDetail[elem.name] = err.Error() continue } metadata.SuccessfulProvider = elem.name return taskID, metadata, nil } } return taskID, metadata, multierror.Append(err, errors.New("failure in FirmwareInstallUploadAndInitiate")) } // FirmwareInstallUploadAndInitiateFromInterfaces identifies implementations of the FirmwareInstallProvider interface and passes the found implementations to the firmwareInstallUploadAndInitiate() wrapper func FirmwareInstallUploadAndInitiateFromInterfaces(ctx context.Context, component string, file *os.File, generic []interface{}) (taskID string, metadata Metadata, err error) { metadata = newMetadata() implementations := make([]firmwareInstallProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := firmwareInstallProvider{name: getProviderName(elem)} switch p := elem.(type) { case FirmwareInstallProvider: temp.FirmwareInstallProvider = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a FirmwareInstallProvider implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return taskID, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no FirmwareInstallProvider implementations found"), ), ) } return firmwareInstallUploadAndInitiate(ctx, component, file, implementations) } // FirmwareInstallerUploaded defines an interface to install firmware that was previously uploaded with FirmwareUpload type FirmwareInstallerUploaded interface { // FirmwareInstallUploaded uploads firmware update payload to the BMC returning the firmware install task ID // // parameters: // component - the component slug for the component update being installed. // uploadTaskID - the taskID for the firmware upload verify task (returned by FirmwareUpload) // // return values: // installTaskID - A installTaskID is returned if the update process on the BMC returns an identifier for the firmware install process. FirmwareInstallUploaded(ctx context.Context, component, uploadTaskID string) (taskID string, err error) } // firmwareInstallerProvider is an internal struct to correlate an implementation/provider and its name type firmwareInstallerWithOptionsProvider struct { name string FirmwareInstallerUploaded } // firmwareInstallUploaded uploads and initiates firmware update for the component func firmwareInstallUploaded(ctx context.Context, component, uploadTaskID string, generic []firmwareInstallerWithOptionsProvider) (installTaskID string, metadata Metadata, err error) { metadata = newMetadata() for _, elem := range generic { if elem.FirmwareInstallerUploaded == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return installTaskID, metadata, err default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) var vErr error installTaskID, vErr = elem.FirmwareInstallUploaded(ctx, component, uploadTaskID) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) metadata.FailedProviderDetail[elem.name] = err.Error() continue } metadata.SuccessfulProvider = elem.name return installTaskID, metadata, nil } } return installTaskID, metadata, multierror.Append(err, errors.New("failure in FirmwareInstallUploaded")) } // FirmwareInstallerUploadedFromInterfaces identifies implementations of the FirmwareInstallUploaded interface and passes the found implementations to the firmwareInstallUploaded() wrapper func FirmwareInstallerUploadedFromInterfaces(ctx context.Context, component, uploadTaskID string, generic []interface{}) (installTaskID string, metadata Metadata, err error) { metadata = newMetadata() implementations := make([]firmwareInstallerWithOptionsProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := firmwareInstallerWithOptionsProvider{name: getProviderName(elem)} switch p := elem.(type) { case FirmwareInstallerUploaded: temp.FirmwareInstallerUploaded = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a FirmwareInstallerUploaded implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return installTaskID, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no FirmwareInstallerUploaded implementations found"), ), ) } return firmwareInstallUploaded(ctx, component, uploadTaskID, implementations) } type FirmwareInstallStepsGetter interface { FirmwareInstallSteps(ctx context.Context, component string) ([]constants.FirmwareInstallStep, error) } // firmwareInstallStepsGetterProvider is an internal struct to correlate an implementation/provider and its name type firmwareInstallStepsGetterProvider struct { name string FirmwareInstallStepsGetter } // FirmwareInstallStepsFromInterfaces identifies implementations of the FirmwareInstallStepsGetter interface and passes the found implementations to the firmwareInstallSteps() wrapper. func FirmwareInstallStepsFromInterfaces(ctx context.Context, component string, generic []interface{}) (steps []constants.FirmwareInstallStep, metadata Metadata, err error) { metadata = newMetadata() implementations := make([]firmwareInstallStepsGetterProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := firmwareInstallStepsGetterProvider{name: getProviderName(elem)} switch p := elem.(type) { case FirmwareInstallStepsGetter: temp.FirmwareInstallStepsGetter = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a FirmwareInstallStepsGetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return steps, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no FirmwareInstallStepsGetter implementations found"), ), ) } return firmwareInstallSteps(ctx, component, implementations) } func firmwareInstallSteps(ctx context.Context, component string, generic []firmwareInstallStepsGetterProvider) (steps []constants.FirmwareInstallStep, metadata Metadata, err error) { metadata = newMetadata() for _, elem := range generic { if elem.FirmwareInstallStepsGetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return steps, metadata, err default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) steps, vErr := elem.FirmwareInstallSteps(ctx, component) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) metadata.FailedProviderDetail[elem.name] = err.Error() continue } metadata.SuccessfulProvider = elem.name return steps, metadata, nil } } return steps, metadata, multierror.Append(err, errors.New("failure in FirmwareInstallSteps")) } type FirmwareUploader interface { FirmwareUpload(ctx context.Context, component string, file *os.File) (uploadVerifyTaskID string, err error) } // firmwareUploaderProvider is an internal struct to correlate an implementation/provider and its name type firmwareUploaderProvider struct { name string FirmwareUploader } // FirmwareUploaderFromInterfaces identifies implementations of the FirmwareUploader interface and passes the found implementations to the firmwareUpload() wrapper. func FirmwareUploadFromInterfaces(ctx context.Context, component string, file *os.File, generic []interface{}) (taskID string, metadata Metadata, err error) { metadata = newMetadata() implementations := make([]firmwareUploaderProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := firmwareUploaderProvider{name: getProviderName(elem)} switch p := elem.(type) { case FirmwareUploader: temp.FirmwareUploader = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a FirmwareUploader implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return taskID, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no FirmwareUploader implementations found"), ), ) } return firmwareUpload(ctx, component, file, implementations) } func firmwareUpload(ctx context.Context, component string, file *os.File, generic []firmwareUploaderProvider) (taskID string, metadata Metadata, err error) { metadata = newMetadata() for _, elem := range generic { if elem.FirmwareUploader == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return taskID, metadata, err default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) taskID, vErr := elem.FirmwareUpload(ctx, component, file) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) metadata.FailedProviderDetail[elem.name] = err.Error() continue } metadata.SuccessfulProvider = elem.name return taskID, metadata, nil } } return taskID, metadata, multierror.Append(err, errors.New("failure in FirmwareUpload")) } // FirmwareTaskVerifier defines an interface to check the status for firmware related tasks queued on the BMC. // these could be a an firmware upload and verify task or a firmware install task. // // This is to replace the FirmwareInstallVerifier interface type FirmwareTaskVerifier interface { // FirmwareTaskStatus returns the status of the firmware upload process. // // parameters: // kind (required) - The FirmwareInstallStep // component (optional) - the component slug for the component that the firmware was uploaded for. // taskID (required) - the task identifier. // installVersion (optional) - the firmware version being installed as part of the task if applicable. // // return values: // state - returns one of the FirmwareTask statuses (see devices/constants.go). // status - returns firmware task progress or other arbitrary task information. FirmwareTaskStatus(ctx context.Context, kind bconsts.FirmwareInstallStep, component, taskID, installVersion string) (state constants.TaskState, status string, err error) } // firmwareTaskVerifierProvider is an internal struct to correlate an implementation/provider and its name type firmwareTaskVerifierProvider struct { name string FirmwareTaskVerifier } // firmwareTaskStatus returns the status of the firmware upload process. func firmwareTaskStatus(ctx context.Context, kind bconsts.FirmwareInstallStep, component, taskID, installVersion string, generic []firmwareTaskVerifierProvider) (state constants.TaskState, status string, metadata Metadata, err error) { metadata = newMetadata() for _, elem := range generic { if elem.FirmwareTaskVerifier == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return state, status, metadata, err default: metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, elem.name) state, status, vErr := elem.FirmwareTaskStatus(ctx, kind, component, taskID, installVersion) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) metadata.FailedProviderDetail[elem.name] = err.Error() continue } metadata.SuccessfulProvider = elem.name return state, status, metadata, nil } } return state, status, metadata, multierror.Append(err, errors.New("failure in FirmwareTaskStatus")) } // FirmwareTaskStatusFromInterfaces identifies implementations of the FirmwareTaskVerifier interface and passes the found implementations to the firmwareTaskStatus() wrapper. func FirmwareTaskStatusFromInterfaces(ctx context.Context, kind bconsts.FirmwareInstallStep, component, taskID, installVersion string, generic []interface{}) (state constants.TaskState, status string, metadata Metadata, err error) { metadata = newMetadata() implementations := make([]firmwareTaskVerifierProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := firmwareTaskVerifierProvider{name: getProviderName(elem)} switch p := elem.(type) { case FirmwareTaskVerifier: temp.FirmwareTaskVerifier = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a FirmwareTaskVerifier implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return state, status, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no FirmwareTaskVerifier implementations found"), ), ) } return firmwareTaskStatus(ctx, kind, component, taskID, installVersion, implementations) } ================================================ FILE: bmc/firmware_test.go ================================================ package bmc import ( "context" "io" "os" "testing" "time" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/bmc-toolbox/common" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) type firmwareInstallTester struct { returnTaskID string returnError error } func (f *firmwareInstallTester) FirmwareInstall(ctx context.Context, component, applyAt string, forceInstall bool, reader io.Reader) (taskID string, err error) { return f.returnTaskID, f.returnError } func (r *firmwareInstallTester) Name() string { return "foo" } func TestFirmwareInstall(t *testing.T) { testCases := []struct { testName string component string applyAt string forceInstall bool reader io.Reader returnTaskID string returnError error ctxTimeout time.Duration providerName string providersAttempted int }{ {"success with metadata", common.SlugBIOS, string(constants.OnReset), false, nil, "1234", nil, 5 * time.Second, "foo", 1}, {"failure with metadata", common.SlugBIOS, string(constants.OnReset), false, nil, "1234", bmclibErrs.ErrNon200Response, 5 * time.Second, "foo", 1}, {"failure with context timeout", common.SlugBIOS, string(constants.OnReset), false, nil, "1234", context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { testImplementation := firmwareInstallTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() taskID, metadata, err := firmwareInstall(ctx, tc.component, tc.applyAt, tc.forceInstall, tc.reader, []firmwareInstallerProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnTaskID, taskID) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } func TestFirmwareInstallFromInterfaces(t *testing.T) { testCases := []struct { testName string component string applyAt string forceInstall bool reader io.Reader returnTaskID string returnError error providerName string badImplementation bool }{ {"success with metadata", common.SlugBIOS, string(constants.OnReset), false, nil, "1234", nil, "foo", false}, {"failure with metadata", common.SlugBIOS, string(constants.OnReset), false, nil, "1234", bmclibErrs.ErrProviderImplementation, "foo", true}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := &firmwareInstallTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} generic = []interface{}{testImplementation} } taskID, metadata, err := FirmwareInstallFromInterfaces(context.Background(), tc.component, tc.applyAt, tc.forceInstall, tc.reader, generic) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnTaskID, taskID) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } type firmwareInstallStatusTester struct { returnStatus string returnError error } func (f *firmwareInstallStatusTester) FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (status string, err error) { return f.returnStatus, f.returnError } func (r *firmwareInstallStatusTester) Name() string { return "foo" } func TestFirmwareInstallStatus(t *testing.T) { testCases := []struct { testName string component string installVersion string taskID string returnStatus string returnError error ctxTimeout time.Duration providerName string providersAttempted int }{ {"success with metadata", common.SlugBIOS, "1.1", "1234", constants.FirmwareInstallComplete, nil, 5 * time.Second, "foo", 1}, {"failure with metadata", common.SlugBIOS, "1.1", "1234", constants.FirmwareInstallFailed, bmclibErrs.ErrNon200Response, 5 * time.Second, "foo", 1}, {"failure with context timeout", common.SlugBIOS, "1.1", "1234", "", context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { testImplementation := firmwareInstallStatusTester{returnStatus: tc.returnStatus, returnError: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 4 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() taskID, metadata, err := firmwareInstallStatus(ctx, tc.installVersion, tc.component, tc.taskID, []firmwareInstallVerifierProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnStatus, taskID) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } func TestFirmwareInstallStatusFromInterfaces(t *testing.T) { testCases := []struct { testName string component string installVersion string taskID string returnStatus string returnError error providerName string badImplementation bool }{ {"success with metadata", common.SlugBIOS, "1.1", "1234", "status-done", nil, "foo", false}, {"failure with bad implementation", common.SlugBIOS, "1.1", "1234", "status-done", bmclibErrs.ErrProviderImplementation, "foo", true}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := &firmwareInstallStatusTester{returnStatus: tc.returnStatus, returnError: tc.returnError} generic = []interface{}{testImplementation} } status, metadata, err := FirmwareInstallStatusFromInterfaces(context.Background(), tc.component, tc.installVersion, tc.taskID, generic) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnStatus, status) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } type firmwareInstallUploadAndInitiateTester struct { returnTaskID string returnError error } func (f *firmwareInstallUploadAndInitiateTester) FirmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File) (taskID string, err error) { return f.returnTaskID, f.returnError } func (r *firmwareInstallUploadAndInitiateTester) Name() string { return "foo" } func TestFirmwareInstallUploadAndInitiate(t *testing.T) { testCases := []struct { testName string component string file *os.File returnTaskID string returnError error ctxTimeout time.Duration providerName string providersAttempted int }{ {"success with metadata", "componentA", &os.File{}, "1234", nil, 5 * time.Second, "foo", 1}, {"failure with metadata", "componentB", &os.File{}, "1234", errors.New("failed to upload and initiate"), 5 * time.Second, "foo", 1}, {"failure with context timeout", "componentC", &os.File{}, "", context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { testImplementation := &firmwareInstallUploadAndInitiateTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() taskID, metadata, err := firmwareInstallUploadAndInitiate(ctx, tc.component, tc.file, []firmwareInstallProvider{{tc.providerName, testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnTaskID, taskID) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } func TestFirmwareInstallUploadAndInitiateFromInterfaces(t *testing.T) { testCases := []struct { testName string component string file *os.File returnTaskID string returnError error providerName string badImplementation bool }{ {"success with metadata", "componentA", &os.File{}, "1234", nil, "foo", false}, {"failure with bad implementation", "componentB", &os.File{}, "1234", bmclibErrs.ErrProviderImplementation, "foo", true}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := &firmwareInstallUploadAndInitiateTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} generic = []interface{}{testImplementation} } taskID, metadata, err := FirmwareInstallUploadAndInitiateFromInterfaces(context.Background(), tc.component, tc.file, generic) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnTaskID, taskID) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } type firmwareInstallUploadTester struct { TaskID string Err error } func (f *firmwareInstallUploadTester) FirmwareInstallUploaded(ctx context.Context, component, uploadTaskID string) (taskID string, err error) { return f.TaskID, f.Err } func (r *firmwareInstallUploadTester) Name() string { return "foo" } func TestFirmwareInstallUploaded(t *testing.T) { testCases := []struct { testName string component string uploadTaskID string returnTaskID string returnError error ctxTimeout time.Duration providerName string providersAttempted int }{ {"success with metadata", common.SlugBIOS, "1234", "5678", nil, 5 * time.Second, "foo", 1}, {"failure with metadata", common.SlugBIOS, "1234", "", bmclibErrs.ErrNon200Response, 5 * time.Second, "foo", 1}, {"failure with context timeout", common.SlugBIOS, "1234", "", context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { mockImplementation := &firmwareInstallUploadTester{TaskID: tc.returnTaskID, Err: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 4 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() taskID, metadata, err := firmwareInstallUploaded(ctx, tc.component, tc.uploadTaskID, []firmwareInstallerWithOptionsProvider{{tc.providerName, mockImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnTaskID, taskID) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } func TestFirmwareInstallerUploadedFromInterfaces(t *testing.T) { testCases := []struct { testName string component string uploadTaskID string returnTaskID string returnError error providerName string badImplementation bool }{ {"success with metadata", common.SlugBIOS, "1234", "5678", nil, "foo", false}, {"failure with bad implementation", common.SlugBIOS, "1234", "", bmclibErrs.ErrProviderImplementation, "foo", true}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { mockImplementation := &firmwareInstallUploadTester{TaskID: tc.returnTaskID, Err: tc.returnError} generic = []interface{}{mockImplementation} } installTaskID, metadata, err := FirmwareInstallerUploadedFromInterfaces(context.Background(), tc.component, tc.uploadTaskID, generic) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnTaskID, installTaskID) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } type firmwareUploadTester struct { returnTaskID string returnError error } func (f *firmwareUploadTester) FirmwareUpload(ctx context.Context, component string, file *os.File) (uploadVerifyTaskID string, err error) { return f.returnTaskID, f.returnError } func (r *firmwareUploadTester) Name() string { return "foo" } func TestFirmwareUpload(t *testing.T) { testCases := []struct { testName string component string file *os.File returnTaskID string returnError error ctxTimeout time.Duration providerName string providersAttempted int }{ {"success with metadata", common.SlugBIOS, nil, "1234", nil, 5 * time.Second, "foo", 1}, {"failure with metadata", common.SlugBIOS, nil, "1234", bmclibErrs.ErrNon200Response, 5 * time.Second, "foo", 1}, {"failure with context timeout", common.SlugBIOS, nil, "1234", context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { testImplementation := firmwareUploadTester{returnTaskID: tc.returnTaskID, returnError: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() taskID, metadata, err := firmwareUpload(ctx, tc.component, tc.file, []firmwareUploaderProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnTaskID, taskID) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } type firmwareInstallStepsGetterTester struct { Steps []constants.FirmwareInstallStep Err error } func (m *firmwareInstallStepsGetterTester) FirmwareInstallSteps(ctx context.Context, component string) ([]constants.FirmwareInstallStep, error) { return m.Steps, m.Err } func (m *firmwareInstallStepsGetterTester) Name() string { return "foo" } func TestFirmwareInstallStepsFromInterfaces(t *testing.T) { testCases := []struct { testName string component string returnSteps []constants.FirmwareInstallStep returnError error providerName string badImplementation bool }{ {"success with metadata", common.SlugBIOS, []constants.FirmwareInstallStep{constants.FirmwareInstallStepUpload, constants.FirmwareInstallStepInstallStatus}, nil, "foo", false}, {"failure with bad implementation", common.SlugBIOS, nil, bmclibErrs.ErrProviderImplementation, "foo", true}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { mockImplementation := &firmwareInstallStepsGetterTester{Steps: tc.returnSteps, Err: tc.returnError} generic = []interface{}{mockImplementation} } steps, metadata, err := FirmwareInstallStepsFromInterfaces(context.Background(), tc.component, generic) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnSteps, steps) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } type firmwareInstallStepsTester struct { returnSteps []constants.FirmwareInstallStep returnError error } func (f *firmwareInstallStepsTester) FirmwareInstallSteps(ctx context.Context, component string) (steps []constants.FirmwareInstallStep, err error) { return f.returnSteps, f.returnError } func (r *firmwareInstallStepsTester) Name() string { return "foo" } func TestFirmwareInstallSteps(t *testing.T) { testCases := []struct { testName string component string returnSteps []constants.FirmwareInstallStep returnError error ctxTimeout time.Duration providerName string providersAttempted int }{ {"success with metadata", common.SlugBIOS, []constants.FirmwareInstallStep{constants.FirmwareInstallStepUpload, constants.FirmwareInstallStepInstallStatus}, nil, 5 * time.Second, "foo", 1}, {"failure with metadata", common.SlugBIOS, nil, bmclibErrs.ErrNon200Response, 5 * time.Second, "foo", 1}, {"failure with context timeout", common.SlugBIOS, nil, context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { testImplementation := firmwareInstallStepsTester{returnSteps: tc.returnSteps, returnError: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() steps, metadata, err := firmwareInstallSteps(ctx, tc.component, []firmwareInstallStepsGetterProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnSteps, steps) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } type firmwareTaskStatusTester struct { returnState constants.TaskState returnStatus string returnError error } func (f *firmwareTaskStatusTester) FirmwareTaskStatus(ctx context.Context, kind constants.FirmwareInstallStep, component, taskID, installVersion string) (state constants.TaskState, status string, err error) { return f.returnState, f.returnStatus, f.returnError } func (r *firmwareTaskStatusTester) Name() string { return "foo" } func TestFirmwareTaskStatus(t *testing.T) { testCases := []struct { testName string kind constants.FirmwareInstallStep component string taskID string installVersion string returnState constants.TaskState returnStatus string returnError error ctxTimeout time.Duration providerName string providersAttempted int }{ {"success with metadata", constants.FirmwareInstallStepUpload, common.SlugBIOS, "1234", "1.0", constants.FirmwareInstallComplete, "Upload completed", nil, 5 * time.Second, "foo", 1}, {"failure with metadata", constants.FirmwareInstallStepUpload, common.SlugBIOS, "1234", "1.0", constants.FirmwareInstallFailed, "Upload failed", bmclibErrs.ErrNon200Response, 5 * time.Second, "foo", 1}, {"failure with context timeout", constants.FirmwareInstallStepUpload, common.SlugBIOS, "1234", "1.0", "", "", context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { testImplementation := firmwareTaskStatusTester{returnState: tc.returnState, returnStatus: tc.returnStatus, returnError: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() state, status, metadata, err := firmwareTaskStatus(ctx, tc.kind, tc.component, tc.taskID, tc.installVersion, []firmwareTaskVerifierProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnState, state) assert.Equal(t, tc.returnStatus, status) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } func TestFirmwareTaskStatusFromInterfaces(t *testing.T) { testCases := []struct { testName string kind constants.FirmwareInstallStep component string taskID string installVersion string returnState constants.TaskState returnStatus string returnError error ctxTimeout time.Duration providerName string providersAttempted int }{ {"success with metadata", constants.FirmwareInstallStepUpload, common.SlugBIOS, "1234", "1.0", constants.Complete, "uploading", nil, 5 * time.Second, "foo", 1}, {"failure with metadata", constants.FirmwareInstallStepUpload, common.SlugBIOS, "1234", "1.0", constants.Failed, "failed", bmclibErrs.ErrNon200Response, 5 * time.Second, "foo", 1}, {"failure with context timeout", constants.FirmwareInstallStepUpload, common.SlugBIOS, "1234", "1.0", "", "", context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { testImplementation := firmwareTaskStatusTester{ returnState: tc.returnState, returnStatus: tc.returnStatus, returnError: tc.returnError, } if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() state, status, metadata, err := FirmwareTaskStatusFromInterfaces(ctx, tc.kind, tc.component, tc.taskID, tc.installVersion, []interface{}{&testImplementation}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnState, state) assert.Equal(t, tc.returnStatus, status) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } ================================================ FILE: bmc/floppy.go ================================================ package bmc import ( "context" "fmt" "io" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) // FloppyImageMounter defines methods to upload a floppy image type FloppyImageMounter interface { MountFloppyImage(ctx context.Context, image io.Reader) (err error) } // floppyImageUploaderProvider is an internal struct to correlate an implementation/provider and its name type floppyImageUploaderProvider struct { name string impl FloppyImageMounter } // mountFloppyImage is a wrapper method to invoke methods for the FloppyImageMounter interface func mountFloppyImage(ctx context.Context, image io.Reader, p []floppyImageUploaderProvider) (metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range p { if elem.impl == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) uploadErr := elem.impl.MountFloppyImage(ctx, image) if uploadErr != nil { err = multierror.Append(err, errors.WithMessagef(uploadErr, "provider: %v", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return metadataLocal, nil } } return metadataLocal, multierror.Append(err, errors.New("failed to mount floppy image")) } // MountFloppyImageFromInterfaces identifies implementations of the FloppyImageMounter interface and passes the found implementations to the mountFloppyImage() wrapper func MountFloppyImageFromInterfaces(ctx context.Context, image io.Reader, p []interface{}) (metadata Metadata, err error) { providers := make([]floppyImageUploaderProvider, 0) for _, elem := range p { if elem == nil { continue } temp := floppyImageUploaderProvider{name: getProviderName(elem)} switch p := elem.(type) { case FloppyImageMounter: temp.impl = p providers = append(providers, temp) default: e := fmt.Sprintf("not a FloppyImageMounter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(providers) == 0 { return metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, "no FloppyImageMounter implementations found", ), ) } return mountFloppyImage(ctx, image, providers) } // FloppyImageMounter defines methods to unmount a floppy image type FloppyImageUnmounter interface { UnmountFloppyImage(ctx context.Context) (err error) } // floppyImageUnmounterProvider is an internal struct to correlate an implementation/provider and its name type floppyImageUnmounterProvider struct { name string impl FloppyImageUnmounter } // unmountFloppyImage is a wrapper method to invoke methods for the FloppyImageUnmounter interface func unmountFloppyImage(ctx context.Context, p []floppyImageUnmounterProvider) (metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range p { if elem.impl == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) uploadErr := elem.impl.UnmountFloppyImage(ctx) if uploadErr != nil { err = multierror.Append(err, errors.WithMessagef(uploadErr, "provider: %v", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return metadataLocal, nil } } return metadataLocal, multierror.Append(err, errors.New("failed to unmount floppy image")) } // MountFloppyImageFromInterfaces identifies implementations of the FloppyImageUnmounter interface and passes the found implementations to the unmountFloppyImage() wrapper func UnmountFloppyImageFromInterfaces(ctx context.Context, p []interface{}) (metadata Metadata, err error) { providers := make([]floppyImageUnmounterProvider, 0) for _, elem := range p { if elem == nil { continue } temp := floppyImageUnmounterProvider{name: getProviderName(elem)} switch p := elem.(type) { case FloppyImageUnmounter: temp.impl = p providers = append(providers, temp) default: e := fmt.Sprintf("not a FloppyImageUnmounter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(providers) == 0 { return metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, "no FloppyImageUnmounter implementations found", ), ) } return unmountFloppyImage(ctx, providers) } ================================================ FILE: bmc/floppy_test.go ================================================ package bmc import ( "context" "io" "testing" "time" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/stretchr/testify/assert" ) type mountFloppyImageTester struct { returnError error } func (p *mountFloppyImageTester) MountFloppyImage(ctx context.Context, reader io.Reader) (err error) { return p.returnError } func (p *mountFloppyImageTester) Name() string { return "foo" } func TestMountFloppyFromInterfaces(t *testing.T) { testCases := []struct { testName string image io.Reader returnError error ctxTimeout time.Duration providerName string providersAttempted int badImplementation bool }{ {"success with metadata", nil, nil, 5 * time.Second, "foo", 1, false}, {"failure with bad implementation", nil, bmclibErrs.ErrProviderImplementation, 1 * time.Nanosecond, "foo", 1, true}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := &mountFloppyImageTester{returnError: tc.returnError} generic = []interface{}{testImplementation} } metadata, err := MountFloppyImageFromInterfaces(context.Background(), tc.image, generic) if tc.returnError != nil { assert.ErrorContains(t, err, tc.returnError.Error()) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnError, err) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } type unmountFloppyImageTester struct { returnError error } func (p *unmountFloppyImageTester) UnmountFloppyImage(ctx context.Context) (err error) { return p.returnError } func (p *unmountFloppyImageTester) Name() string { return "foo" } func TestUnmountFloppyFromInterfaces(t *testing.T) { testCases := []struct { testName string returnError error ctxTimeout time.Duration providerName string providersAttempted int badImplementation bool }{ {"success with metadata", nil, 5 * time.Second, "foo", 1, false}, {"failure with bad implementation", bmclibErrs.ErrProviderImplementation, 1 * time.Nanosecond, "foo", 1, true}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := &unmountFloppyImageTester{returnError: tc.returnError} generic = []interface{}{testImplementation} } metadata, err := UnmountFloppyImageFromInterfaces(context.Background(), generic) if tc.returnError != nil { assert.ErrorContains(t, err, tc.returnError.Error()) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnError, err) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } ================================================ FILE: bmc/inventory.go ================================================ package bmc import ( "context" "fmt" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/bmc-toolbox/common" ) // InventoryGetter defines methods to retrieve device hardware and firmware inventory type InventoryGetter interface { Inventory(ctx context.Context) (device *common.Device, err error) } type inventoryGetterProvider struct { name string InventoryGetter } // inventory returns hardware and firmware inventory func inventory(ctx context.Context, generic []inventoryGetterProvider) (device *common.Device, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range generic { if elem.InventoryGetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return device, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) device, vErr := elem.Inventory(ctx) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) continue } metadataLocal.SuccessfulProvider = elem.name return device, metadataLocal, nil } } return device, metadataLocal, multierror.Append(err, errors.New("failure to get device inventory")) } // GetInventoryFromInterfaces identifies implementations of the InventoryGetter interface and passes the found implementations to the inventory() wrapper method func GetInventoryFromInterfaces(ctx context.Context, generic []interface{}) (device *common.Device, metadata Metadata, err error) { metadata = newMetadata() implementations := make([]inventoryGetterProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := inventoryGetterProvider{name: getProviderName(elem)} switch p := elem.(type) { case InventoryGetter: temp.InventoryGetter = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a InventoryGetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return device, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no InventoryGetter implementations found"), ), ) } return inventory(ctx, implementations) } ================================================ FILE: bmc/inventory_test.go ================================================ package bmc import ( "context" "testing" "time" "github.com/bmc-toolbox/bmclib/v2/errors" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/bmc-toolbox/common" "github.com/stretchr/testify/assert" ) type inventoryGetterTester struct { returnDevice *common.Device returnError error } func (f *inventoryGetterTester) Inventory(ctx context.Context) (device *common.Device, err error) { return f.returnDevice, f.returnError } func (f *inventoryGetterTester) Name() string { return "foo" } func TestInventory(t *testing.T) { testCases := []struct { testName string returnDevice *common.Device returnError error ctxTimeout time.Duration providerName string providersAttempted int }{ {"success with metadata", &common.Device{Common: common.Common{Vendor: "foo"}}, nil, 5 * time.Second, "foo", 1}, {"failure with metadata", nil, errors.ErrNon200Response, 5 * time.Second, "foo", 1}, {"failure with context timeout", nil, context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { testImplementation := inventoryGetterTester{returnDevice: tc.returnDevice, returnError: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() device, metadata, err := inventory(ctx, []inventoryGetterProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnDevice, device) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } func TestInventoryFromInterfaces(t *testing.T) { testCases := []struct { testName string returnDevice *common.Device returnError error ctxTimeout time.Duration providerName string providersAttempted int badImplementation bool }{ {"success with metadata", &common.Device{Common: common.Common{Vendor: "foo"}}, nil, 5 * time.Second, "foo", 1, false}, {"failure with bad implementation", nil, bmclibErrs.ErrProviderImplementation, 5 * time.Second, "foo", 1, true}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := &inventoryGetterTester{returnDevice: tc.returnDevice, returnError: tc.returnError} generic = []interface{}{testImplementation} } device, metadata, err := GetInventoryFromInterfaces(context.Background(), generic) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnDevice, device) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } ================================================ FILE: bmc/nmi.go ================================================ package bmc import ( "context" "errors" "fmt" "time" "github.com/hashicorp/go-multierror" ) type NMISender interface { SendNMI(ctx context.Context) error } func sendNMI(ctx context.Context, timeout time.Duration, sender NMISender, metadata *Metadata) error { senderName := getProviderName(sender) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() metadata.ProvidersAttempted = append(metadata.ProvidersAttempted, senderName) err := sender.SendNMI(ctx) if err != nil { metadata.FailedProviderDetail[senderName] = err.Error() return err } metadata.SuccessfulProvider = senderName return nil } // SendNMIFromInterface will look for providers that implement NMISender // and attempt to call SendNMI until a provider is successful, // or all providers have been exhausted. func SendNMIFromInterface( ctx context.Context, timeout time.Duration, providers []interface{}, ) (metadata Metadata, err error) { metadata = newMetadata() for _, provider := range providers { sender, ok := provider.(NMISender) if !ok { err = multierror.Append(err, fmt.Errorf("not an NMISender implementation: %T", provider)) continue } sendNMIErr := sendNMI(ctx, timeout, sender, &metadata) if sendNMIErr != nil { err = multierror.Append(err, sendNMIErr) continue } return metadata, nil } if len(metadata.ProvidersAttempted) == 0 { err = multierror.Append(err, errors.New("no NMISender implementations found")) } else { err = multierror.Append(err, errors.New("failed to send NMI")) } return metadata, err } ================================================ FILE: bmc/nmi_test.go ================================================ package bmc import ( "context" "testing" "time" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) type mockNMISender struct { err error } func (m *mockNMISender) SendNMI(ctx context.Context) error { select { case <-ctx.Done(): return ctx.Err() default: return m.err } } func (m *mockNMISender) Name() string { return "mock" } func TestSendNMIFromInterface(t *testing.T) { testCases := []struct { name string mockSenders []interface{} errMsg string isTimedout bool expectedMetadata Metadata }{ { name: "success", mockSenders: []interface{}{&mockNMISender{}}, expectedMetadata: Metadata{ SuccessfulProvider: "mock", ProvidersAttempted: []string{"mock"}, FailedProviderDetail: make(map[string]string), }, }, { name: "success with multiple senders", mockSenders: []interface{}{ nil, "foo", &mockNMISender{err: errors.New("err from sender")}, &mockNMISender{}, }, expectedMetadata: Metadata{ SuccessfulProvider: "mock", ProvidersAttempted: []string{"mock", "mock"}, FailedProviderDetail: map[string]string{"mock": "err from sender"}, }, }, { name: "not an nmisender", mockSenders: []interface{}{nil}, errMsg: "not an NMISender", expectedMetadata: Metadata{ FailedProviderDetail: make(map[string]string), }, }, { name: "no nmisenders", mockSenders: []interface{}{}, errMsg: "no NMISender implementations found", expectedMetadata: Metadata{ FailedProviderDetail: make(map[string]string), }, }, { name: "timed out", mockSenders: []interface{}{&mockNMISender{}}, isTimedout: true, errMsg: "context deadline exceeded", expectedMetadata: Metadata{ ProvidersAttempted: []string{"mock"}, FailedProviderDetail: map[string]string{"mock": "context deadline exceeded"}, }, }, { name: "error from nmisender", mockSenders: []interface{}{&mockNMISender{err: errors.New("foobar")}}, errMsg: "foobar", expectedMetadata: Metadata{ ProvidersAttempted: []string{"mock"}, FailedProviderDetail: map[string]string{"mock": "foobar"}, }, }, { name: "error when fail to send", mockSenders: []interface{}{&mockNMISender{err: errors.New("err from sender")}}, errMsg: "failed to send NMI", expectedMetadata: Metadata{ ProvidersAttempted: []string{"mock"}, FailedProviderDetail: map[string]string{"mock": "err from sender"}, }, }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { timeout := time.Second * 60 if tt.isTimedout { timeout = 0 } metadata, err := SendNMIFromInterface(context.Background(), timeout, tt.mockSenders) if tt.errMsg == "" { assert.NoError(t, err) } else { assert.ErrorContains(t, err, tt.errMsg) } assert.Equal(t, tt.expectedMetadata, metadata) }) } } ================================================ FILE: bmc/postcode.go ================================================ package bmc import ( "context" "fmt" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) // PostCodeGetter defines methods to retrieve device BIOS/UEFI POST code type PostCodeGetter interface { // PostCode retrieves the BIOS/UEFI POST code from a device // // returns 'status' which is a (bmclib specific) string identifier for the POST code // and 'code' with the actual POST code returned to bmclib by the device PostCode(ctx context.Context) (status string, code int, err error) } type postCodeGetterProvider struct { name string PostCodeGetter } // postCode returns the device BIOS/UEFI POST code func postCode(ctx context.Context, generic []postCodeGetterProvider) (status string, code int, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range generic { if elem.PostCodeGetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return status, code, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) status, code, vErr := elem.PostCode(ctx) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) err = multierror.Append(err, vErr) continue } metadataLocal.SuccessfulProvider = elem.name return status, code, metadataLocal, nil } } return status, code, metadataLocal, multierror.Append(err, errors.New("failure to get device POST code")) } // GetPostCodeFromInterfaces identifies implementations of the PostCodeGetter interface and passes the found implementations to the postCode() wrapper method. func GetPostCodeInterfaces(ctx context.Context, generic []interface{}) (status string, code int, metadata Metadata, err error) { implementations := make([]postCodeGetterProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := postCodeGetterProvider{name: getProviderName(elem)} switch p := elem.(type) { case PostCodeGetter: temp.PostCodeGetter = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a PostCodeGetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return status, code, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no PostCodeGetter implementations found"), ), ) } return postCode(ctx, implementations) } ================================================ FILE: bmc/postcode_test.go ================================================ package bmc import ( "context" "testing" "time" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/stretchr/testify/assert" ) type postCodeGetterTester struct { returnStatus string returnCode int returnError error } func (p *postCodeGetterTester) PostCode(ctx context.Context) (status string, code int, err error) { return p.returnStatus, p.returnCode, p.returnError } func (p *postCodeGetterTester) Name() string { return "foo" } func TestPostCode(t *testing.T) { testCases := []struct { testName string returnStatus string returnCode int returnError error ctxTimeout time.Duration providerName string providersAttempted int }{ {"success with metadata", constants.POSTStateOS, 164, nil, 5 * time.Second, "foo", 1}, {"failure with metadata", constants.POSTCodeUnknown, 0, bmclibErrs.ErrNon200Response, 5 * time.Second, "foo", 1}, {"failure with context timeout", "", 0, context.DeadlineExceeded, 1 * time.Nanosecond, "foo", 1}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { testImplementation := postCodeGetterTester{returnStatus: tc.returnStatus, returnCode: tc.returnCode, returnError: tc.returnError} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() status, code, metadata, err := postCode(ctx, []postCodeGetterProvider{{tc.providerName, &testImplementation}}) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnStatus, status) assert.Equal(t, tc.returnCode, code) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) assert.Equal(t, tc.providersAttempted, len(metadata.ProvidersAttempted)) }) } } func TestPostCodeFromInterfaces(t *testing.T) { testCases := []struct { testName string returnStatus string returnCode int returnError error ctxTimeout time.Duration providerName string providersAttempted int badImplementation bool }{ {"success with metadata", constants.POSTStateOS, 164, nil, 5 * time.Second, "foo", 1, false}, {"failure with bad implementation", "", 0, bmclibErrs.ErrProviderImplementation, 1 * time.Nanosecond, "foo", 1, true}, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := &postCodeGetterTester{returnStatus: tc.returnStatus, returnCode: tc.returnCode, returnError: tc.returnError} generic = []interface{}{testImplementation} } status, code, metadata, err := GetPostCodeInterfaces(context.Background(), generic) if tc.returnError != nil { assert.ErrorIs(t, err, tc.returnError) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.returnStatus, status) assert.Equal(t, tc.returnCode, code) assert.Equal(t, tc.providerName, metadata.SuccessfulProvider) }) } } ================================================ FILE: bmc/power.go ================================================ package bmc import ( "context" "fmt" "time" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) // PowerSetter sets the power state of a BMC type PowerSetter interface { // PowerSet sets the power state of a Machine through a BMC. // While the state's accepted are ultimately up to what the implementation // expects, implementations should generally try to provide support for the following // states, modeled after the functionality available in `ipmitool chassis power`. // // "on": Power up chassis. should not error if the machine is already on // "off": Hard powers down chassis. should not error if the machine is already off // "soft": Initiate a soft-shutdown of OS via ACPI. // "reset": soft down and then power on. simulates a reboot from the host OS. // "cycle": hard power down followed by a power on. simulates pressing a power button // to turn the machine off then pressing the button again to turn it on. PowerSet(ctx context.Context, state string) (ok bool, err error) } // PowerStateGetter gets the power state of a BMC type PowerStateGetter interface { PowerStateGet(ctx context.Context) (state string, err error) } // powerProviders is an internal struct to correlate an implementation/provider and its name type powerProviders struct { name string powerStateGetter PowerStateGetter powerSetter PowerSetter } // setPowerState sets the power state for a BMC, trying all interface implementations passed in func setPowerState(ctx context.Context, timeout time.Duration, state string, p []powerProviders) (ok bool, m Metadata, err error) { metadataLocal := Metadata{ FailedProviderDetail: make(map[string]string), } for _, elem := range p { if elem.powerSetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return false, metadataLocal, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ok, setErr := elem.powerSetter.PowerSet(ctx, state) if setErr != nil { err = multierror.Append(err, errors.WithMessagef(setErr, "provider: %v", elem.name)) metadataLocal.FailedProviderDetail[elem.name] = setErr.Error() continue } if !ok { err = multierror.Append(err, fmt.Errorf("provider: %v, failed to set power state", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return ok, metadataLocal, nil } } return ok, metadataLocal, multierror.Append(err, errors.New("failed to set power state")) } // SetPowerStateFromInterfaces identifies implementations of the PostStateSetter interface and passes the found implementations to the setPowerState() wrapper. func SetPowerStateFromInterfaces(ctx context.Context, timeout time.Duration, state string, generic []interface{}) (ok bool, metadata Metadata, err error) { metadata = newMetadata() powerSetter := make([]powerProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := powerProviders{name: getProviderName(elem)} switch p := elem.(type) { case PowerSetter: temp.powerSetter = p powerSetter = append(powerSetter, temp) default: e := fmt.Sprintf("not a PowerSetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(powerSetter) == 0 { return ok, metadata, multierror.Append(err, errors.New("no PowerSetter implementations found")) } return setPowerState(ctx, timeout, state, powerSetter) } // getPowerState gets the power state for a BMC, trying all interface implementations passed in func getPowerState(ctx context.Context, timeout time.Duration, p []powerProviders) (state string, m Metadata, err error) { metadataLocal := Metadata{ FailedProviderDetail: make(map[string]string), } for _, elem := range p { if elem.powerStateGetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return state, metadataLocal, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() state, stateErr := elem.powerStateGetter.PowerStateGet(ctx) if stateErr != nil { err = multierror.Append(err, errors.WithMessagef(stateErr, "provider: %v", elem.name)) metadataLocal.FailedProviderDetail[elem.name] = stateErr.Error() continue } metadataLocal.SuccessfulProvider = elem.name return state, metadataLocal, nil } } return state, metadataLocal, multierror.Append(err, errors.New("failed to get power state")) } // GetPowerStateFromInterfaces identifies implementations of the PostStateGetter interface and passes the found implementations to the getPowerState() wrapper. func GetPowerStateFromInterfaces(ctx context.Context, timeout time.Duration, generic []interface{}) (state string, metadata Metadata, err error) { metadata = newMetadata() powerStateGetter := make([]powerProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := powerProviders{name: getProviderName(elem)} switch p := elem.(type) { case PowerStateGetter: temp.powerStateGetter = p powerStateGetter = append(powerStateGetter, temp) default: e := fmt.Sprintf("not a PowerStateGetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(powerStateGetter) == 0 { return state, metadata, multierror.Append(err, errors.New("no PowerStateGetter implementations found")) } return getPowerState(ctx, timeout, powerStateGetter) } ================================================ FILE: bmc/power_test.go ================================================ package bmc import ( "context" "errors" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-multierror" ) type powerTester struct { MakeNotOK bool MakeErrorOut bool } func (p *powerTester) PowerSet(ctx context.Context, state string) (ok bool, err error) { if p.MakeErrorOut { return ok, errors.New("power set failed") } if p.MakeNotOK { return false, nil } return true, nil } func (p *powerTester) PowerStateGet(ctx context.Context) (state string, err error) { if p.MakeErrorOut { return state, errors.New("power state get failed") } return "on", nil } func (p *powerTester) Name() string { return "test provider" } func TestSetPowerState(t *testing.T) { testCases := map[string]struct { state string makeErrorOut bool makeNotOk bool want bool err error ctxTimeout time.Duration }{ "success": {state: "off", want: true}, "not ok return": {state: "off", want: false, makeNotOk: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider, failed to set power state"), errors.New("failed to set power state")}}}, "error": {state: "off", want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider: power set failed"), errors.New("failed to set power state")}}}, "error context timeout": {state: "off", want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := powerTester{MakeErrorOut: tc.makeErrorOut, MakeNotOK: tc.makeNotOk} expectedResult := tc.want if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() result, _, err := setPowerState(ctx, 0, tc.state, []powerProviders{{"test provider", nil, &testImplementation}}) if err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } }) } } func TestSetPowerStateFromInterfaces(t *testing.T) { testCases := map[string]struct { state string err error badImplementation bool want bool withMetadata bool }{ "success": {state: "off", want: true}, "success with metadata": {state: "on", want: true, withMetadata: true}, "no implementations found": {state: "on", want: false, badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a PowerSetter implementation: *struct {}"), errors.New("no PowerSetter implementations found")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := powerTester{} generic = []interface{}{&testImplementation} } expectedResult := tc.want result, metadata, err := SetPowerStateFromInterfaces(context.Background(), 0, tc.state, generic) if err != nil { if tc.err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } if tc.withMetadata { if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { t.Fatal(diff) } } }) } } func TestGetPowerState(t *testing.T) { testCases := map[string]struct { state string makeFail bool err error ctxTimeout time.Duration }{ "success": {state: "on", err: nil}, "failure": {state: "on", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider: power state get failed"), errors.New("failed to get power state")}}}, "fail context timeout": {state: "on", makeFail: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := powerTester{MakeErrorOut: tc.makeFail} expectedResult := tc.state if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() result, _, err := getPowerState(ctx, 0, []powerProviders{{"test provider", &testImplementation, nil}}) if err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } }) } } func TestGetPowerStateFromInterfaces(t *testing.T) { testCases := map[string]struct { state string err error badImplementation bool want string withMetadata bool }{ "success": {state: "on", want: "on"}, "success with metadata": {state: "on", want: "on", withMetadata: true}, "no implementations found": {state: "on", want: "", badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a PowerStateGetter implementation: *struct {}"), errors.New("no PowerStateGetter implementations found")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := powerTester{} generic = []interface{}{&testImplementation} } expectedResult := tc.want result, metadata, err := GetPowerStateFromInterfaces(context.Background(), 0, generic) if err != nil { if tc.err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } if tc.withMetadata { if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { t.Fatal(diff) } } }) } } ================================================ FILE: bmc/provider.go ================================================ package bmc import "fmt" // Provider interface describes details about a provider type Provider interface { // Name of the provider Name() string } // getProviderName returns the name a provider supplies if they implement the Provider interface // if not implemented then the concrete type is returned func getProviderName(provider interface{}) string { if provider == nil { return "" } switch p := provider.(type) { case Provider: return p.Name() } return fmt.Sprintf("%T", provider) } ================================================ FILE: bmc/reset.go ================================================ package bmc import ( "context" "fmt" "time" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) // BMCResetter for resetting a BMC. // resetType: "warm" resets the management console without rebooting the BMC // resetType: "cold" reboots the BMC type BMCResetter interface { BmcReset(ctx context.Context, resetType string) (ok bool, err error) } // bmcProviders is an internal struct to correlate an implementation/provider and its name type bmcProviders struct { name string bmcResetter BMCResetter } // resetBMC tries all implementations for a success BMC reset func resetBMC(ctx context.Context, timeout time.Duration, resetType string, b []bmcProviders) (ok bool, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range b { if elem.bmcResetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return false, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ok, setErr := elem.bmcResetter.BmcReset(ctx, resetType) if setErr != nil { err = multierror.Append(err, errors.WithMessagef(setErr, "provider: %v", elem.name)) continue } if !ok { err = multierror.Append(err, fmt.Errorf("provider: %v, failed to reset BMC", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return ok, metadataLocal, nil } } return ok, metadataLocal, multierror.Append(err, errors.New("failed to reset BMC")) } // ResetBMCFromInterfaces identifies implementations of the BMCResetter interface and passes them to the resetBMC() wrapper method. func ResetBMCFromInterfaces(ctx context.Context, timeout time.Duration, resetType string, generic []interface{}) (ok bool, metadata Metadata, err error) { metadata = newMetadata() bmcSetters := make([]bmcProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := bmcProviders{name: getProviderName(elem)} switch p := elem.(type) { case BMCResetter: temp.bmcResetter = p bmcSetters = append(bmcSetters, temp) default: e := fmt.Sprintf("not a BMCResetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(bmcSetters) == 0 { return ok, metadata, multierror.Append(err, errors.New("no BMCResetter implementations found")) } return resetBMC(ctx, timeout, resetType, bmcSetters) } ================================================ FILE: bmc/reset_test.go ================================================ package bmc import ( "context" "errors" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-multierror" ) type resetTester struct { MakeNotOK bool MakeErrorOut bool } func (r *resetTester) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { if r.MakeErrorOut { return ok, errors.New("bmc reset failed") } if r.MakeNotOK { return false, nil } return true, nil } func (r *resetTester) Name() string { return "test provider" } func TestResetBMC(t *testing.T) { testCases := map[string]struct { resetType string makeErrorOut bool makeNotOk bool want bool err error ctxTimeout time.Duration }{ "success": {resetType: "cold", want: true}, "not ok return": {resetType: "warm", want: false, makeNotOk: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider, failed to reset BMC"), errors.New("failed to reset BMC")}}}, "error": {resetType: "cold", want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider: bmc reset failed"), errors.New("failed to reset BMC")}}}, "error context timeout": {resetType: "cold", want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := resetTester{MakeErrorOut: tc.makeErrorOut, MakeNotOK: tc.makeNotOk} expectedResult := tc.want if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() result, _, err := resetBMC(ctx, 0, tc.resetType, []bmcProviders{{"test provider", &testImplementation}}) if err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } }) } } func TestResetBMCFromInterfaces(t *testing.T) { testCases := map[string]struct { resetType string err error badImplementation bool want bool withName bool }{ "success": {resetType: "cold", want: true}, "success with metadata": {resetType: "cold", want: true, withName: true}, "no implementations found": {resetType: "warm", want: false, badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a BMCResetter implementation: *struct {}"), errors.New("no BMCResetter implementations found")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := resetTester{} generic = []interface{}{&testImplementation} } expectedResult := tc.want result, metadata, err := ResetBMCFromInterfaces(context.Background(), 0, tc.resetType, generic) if err != nil { if tc.err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } if tc.withName { if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { t.Fatal(diff) } } }) } } ================================================ FILE: bmc/screenshot.go ================================================ package bmc import ( "context" "fmt" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) // ScreenshotGetter interface provides methods to query for a BMC screen capture. type ScreenshotGetter interface { Screenshot(ctx context.Context) (image []byte, fileType string, err error) } type screenshotGetterProvider struct { name string ScreenshotGetter } // screenshot returns an image capture of the video output. func screenshot(ctx context.Context, generic []screenshotGetterProvider) (image []byte, fileType string, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range generic { if elem.ScreenshotGetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return image, fileType, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) image, fileType, vErr := elem.Screenshot(ctx) if vErr != nil { err = multierror.Append(err, errors.WithMessagef(vErr, "provider: %v", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return image, fileType, metadataLocal, nil } } return image, fileType, metadataLocal, multierror.Append(err, errors.New("failed to capture screenshot")) } // ScreenshotFromInterfaces identifies implementations of the ScreenshotGetter interface and passes the found implementations to the screenshot() wrapper method. func ScreenshotFromInterfaces(ctx context.Context, generic []interface{}) (image []byte, fileType string, metadata Metadata, err error) { implementations := make([]screenshotGetterProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := screenshotGetterProvider{name: getProviderName(elem)} switch p := elem.(type) { case ScreenshotGetter: temp.ScreenshotGetter = p implementations = append(implementations, temp) default: e := fmt.Sprintf("not a ScreenshotGetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(implementations) == 0 { return image, fileType, metadata, multierror.Append( err, errors.Wrap( bmclibErrs.ErrProviderImplementation, ("no ScreenshotGetter implementations found"), ), ) } return screenshot(ctx, implementations) } ================================================ FILE: bmc/screenshot_test.go ================================================ package bmc import ( "context" "errors" "testing" "time" "github.com/hashicorp/go-multierror" "gopkg.in/go-playground/assert.v1" ) type screenshotTester struct { MakeErrorOut bool } func (r *screenshotTester) Screenshot(ctx context.Context) (img []byte, fileType string, err error) { if r.MakeErrorOut { return nil, "", errors.New("crappy bmc is crappy") } return []byte(`foobar`), "png", nil } func (r *screenshotTester) Name() string { return "test screenshot provider" } func TestScreenshot(t *testing.T) { testCases := map[string]struct { makeErrorOut bool wantImage []byte wantFileType string wantSuccessfulProvider string wantProvidersAttempted []string wantErr error ctxTimeout time.Duration }{ "success": {false, []byte(`foobar`), "png", "test provider", []string{"test provider"}, nil, 1 * time.Second}, "error": {true, nil, "", "", []string{"test provider"}, &multierror.Error{Errors: []error{errors.New("provider: test provider: crappy bmc is crappy"), errors.New("failed to capture screenshot")}}, 1 * time.Second}, "error context timeout": {true, nil, "", "", nil, &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, 1 * time.Nanosecond}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := screenshotTester{MakeErrorOut: tc.makeErrorOut} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() image, fileType, metadata, err := screenshot(ctx, []screenshotGetterProvider{{"test provider", &testImplementation}}) if err != nil { if tc.wantErr == nil { t.Fatal(err) } assert.Equal(t, tc.wantErr.Error(), err.Error()) } else { assert.Equal(t, tc.wantImage, image) assert.Equal(t, tc.wantFileType, fileType) } assert.Equal(t, tc.wantProvidersAttempted, metadata.ProvidersAttempted) assert.Equal(t, tc.wantSuccessfulProvider, metadata.SuccessfulProvider) }) } } func TestScreenshotFromInterfaces(t *testing.T) { testCases := map[string]struct { wantImage []byte wantFileType string wantSuccessfulProvider string wantProvidersAttempted []string wantErr error badImplementation bool }{ "success with metadata": {[]byte(`foobar`), "png", "test screenshot provider", []string{"test screenshot provider"}, nil, false}, "no implementations found": {nil, "", "", nil, &multierror.Error{Errors: []error{errors.New("not a ScreenshotGetter implementation: *struct {}"), errors.New("no ScreenshotGetter implementations found: error in provider implementation")}}, true}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := screenshotTester{} generic = []interface{}{&testImplementation} } image, fileType, metadata, err := ScreenshotFromInterfaces(context.Background(), generic) if err != nil { if tc.wantErr == nil { t.Fatal(err) } assert.Equal(t, tc.wantErr.Error(), err.Error()) } else { assert.Equal(t, tc.wantImage, image) assert.Equal(t, tc.wantFileType, fileType) } assert.Equal(t, tc.wantProvidersAttempted, metadata.ProvidersAttempted) assert.Equal(t, tc.wantSuccessfulProvider, metadata.SuccessfulProvider) }) } } ================================================ FILE: bmc/sel.go ================================================ package bmc import ( "context" "fmt" "time" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) // System Event Log Services for related services type SystemEventLog interface { ClearSystemEventLog(ctx context.Context) (err error) GetSystemEventLog(ctx context.Context) (entries [][]string, err error) GetSystemEventLogRaw(ctx context.Context) (eventlog string, err error) } type systemEventLogProviders struct { name string systemEventLogProvider SystemEventLog } type SystemEventLogEntries [][]string func clearSystemEventLog(ctx context.Context, timeout time.Duration, s []systemEventLogProviders) (metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range s { if elem.systemEventLogProvider == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() selErr := elem.systemEventLogProvider.ClearSystemEventLog(ctx) if selErr != nil { err = multierror.Append(err, errors.WithMessagef(selErr, "provider: %v", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return metadataLocal, nil } } return metadataLocal, multierror.Append(err, errors.New("failed to reset System Event Log")) } func ClearSystemEventLogFromInterfaces(ctx context.Context, timeout time.Duration, generic []interface{}) (metadata Metadata, err error) { selServices := make([]systemEventLogProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := systemEventLogProviders{name: getProviderName(elem)} switch p := elem.(type) { case SystemEventLog: temp.systemEventLogProvider = p selServices = append(selServices, temp) default: e := fmt.Sprintf("not a SystemEventLog service implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(selServices) == 0 { return metadata, multierror.Append(err, errors.New("no SystemEventLog implementations found")) } return clearSystemEventLog(ctx, timeout, selServices) } func getSystemEventLog(ctx context.Context, timeout time.Duration, s []systemEventLogProviders) (sel SystemEventLogEntries, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range s { if elem.systemEventLogProvider == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return sel, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() sel, selErr := elem.systemEventLogProvider.GetSystemEventLog(ctx) if selErr != nil { err = multierror.Append(err, errors.WithMessagef(selErr, "provider: %v", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return sel, metadataLocal, nil } } return nil, metadataLocal, multierror.Append(err, errors.New("failed to get System Event Log")) } func GetSystemEventLogFromInterfaces(ctx context.Context, timeout time.Duration, generic []interface{}) (sel SystemEventLogEntries, metadata Metadata, err error) { selServices := make([]systemEventLogProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := systemEventLogProviders{name: getProviderName(elem)} switch p := elem.(type) { case SystemEventLog: temp.systemEventLogProvider = p selServices = append(selServices, temp) default: e := fmt.Sprintf("not a SystemEventLog service implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(selServices) == 0 { return sel, metadata, multierror.Append(err, errors.New("no SystemEventLog implementations found")) } return getSystemEventLog(ctx, timeout, selServices) } func getSystemEventLogRaw(ctx context.Context, timeout time.Duration, s []systemEventLogProviders) (eventlog string, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range s { if elem.systemEventLogProvider == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return eventlog, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() eventlog, selErr := elem.systemEventLogProvider.GetSystemEventLogRaw(ctx) if selErr != nil { err = multierror.Append(err, errors.WithMessagef(selErr, "provider: %v", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return eventlog, metadataLocal, nil } } return eventlog, metadataLocal, multierror.Append(err, errors.New("failed to get System Event Log")) } func GetSystemEventLogRawFromInterfaces(ctx context.Context, timeout time.Duration, generic []interface{}) (eventlog string, metadata Metadata, err error) { selServices := make([]systemEventLogProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := systemEventLogProviders{name: getProviderName(elem)} switch p := elem.(type) { case SystemEventLog: temp.systemEventLogProvider = p selServices = append(selServices, temp) default: e := fmt.Sprintf("not a SystemEventLog service implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(selServices) == 0 { return eventlog, metadata, multierror.Append(err, errors.New("no SystemEventLog implementations found")) } return getSystemEventLogRaw(ctx, timeout, selServices) } ================================================ FILE: bmc/sel_test.go ================================================ package bmc import ( "context" "testing" "time" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) type mockSystemEventLogService struct { name string err error } func (m *mockSystemEventLogService) ClearSystemEventLog(ctx context.Context) error { return m.err } func (m *mockSystemEventLogService) GetSystemEventLog(ctx context.Context) (entries [][]string, err error) { return nil, m.err } func (m *mockSystemEventLogService) GetSystemEventLogRaw(ctx context.Context) (eventlog string, err error) { return "", m.err } func (m *mockSystemEventLogService) Name() string { return m.name } func TestClearSystemEventLog(t *testing.T) { ctx := context.Background() timeout := 1 * time.Second // Test with a mock SystemEventLogService that returns nil mockService := &mockSystemEventLogService{name: "mock1", err: nil} metadata, err := clearSystemEventLog(ctx, timeout, []systemEventLogProviders{{name: mockService.name, systemEventLogProvider: mockService}}) assert.Nil(t, err) assert.Equal(t, mockService.name, metadata.SuccessfulProvider) // Test with a mock SystemEventLogService that returns an error mockService = &mockSystemEventLogService{name: "mock2", err: errors.New("mock error")} metadata, err = clearSystemEventLog(ctx, timeout, []systemEventLogProviders{{name: mockService.name, systemEventLogProvider: mockService}}) assert.NotNil(t, err) assert.NotEqual(t, mockService.name, metadata.SuccessfulProvider) } func TestClearSystemEventLogFromInterfaces(t *testing.T) { ctx := context.Background() timeout := 1 * time.Second // Test with an empty slice metadata, err := ClearSystemEventLogFromInterfaces(ctx, timeout, []interface{}{}) assert.NotNil(t, err) assert.Empty(t, metadata.SuccessfulProvider) // Test with a slice containing a non-SystemEventLog object metadata, err = ClearSystemEventLogFromInterfaces(ctx, timeout, []interface{}{"not a SystemEventLog Service"}) assert.NotNil(t, err) assert.Empty(t, metadata.SuccessfulProvider) // Test with a slice containing a mock SystemEventLogService that returns nil mockService := &mockSystemEventLogService{name: "mock1"} metadata, err = ClearSystemEventLogFromInterfaces(ctx, timeout, []interface{}{mockService}) assert.Nil(t, err) assert.Equal(t, mockService.name, metadata.SuccessfulProvider) } func TestGetSystemEventLog(t *testing.T) { ctx := context.Background() timeout := 1 * time.Second // Test with a mock SystemEventLogService that returns nil mockService := &mockSystemEventLogService{name: "mock1", err: nil} _, _, err := getSystemEventLog(ctx, timeout, []systemEventLogProviders{{name: mockService.name, systemEventLogProvider: mockService}}) assert.Nil(t, err) // Test with a mock SystemEventLogService that returns an error mockService = &mockSystemEventLogService{name: "mock2", err: errors.New("mock error")} _, _, err = getSystemEventLog(ctx, timeout, []systemEventLogProviders{{name: mockService.name, systemEventLogProvider: mockService}}) assert.NotNil(t, err) } func TestGetSystemEventLogFromInterfaces(t *testing.T) { ctx := context.Background() timeout := 1 * time.Second // Test with an empty slice _, _, err := GetSystemEventLogFromInterfaces(ctx, timeout, []interface{}{}) assert.NotNil(t, err) // Test with a slice containing a non-SystemEventLog object _, _, err = GetSystemEventLogFromInterfaces(ctx, timeout, []interface{}{"not a SystemEventLog Service"}) assert.NotNil(t, err) // Test with a slice containing a mock SystemEventLogService that returns nil mockService := &mockSystemEventLogService{name: "mock1"} _, _, err = GetSystemEventLogFromInterfaces(ctx, timeout, []interface{}{mockService}) assert.Nil(t, err) } func TestGetSystemEventLogRaw(t *testing.T) { ctx := context.Background() timeout := 1 * time.Second // Test with a mock SystemEventLogService that returns nil mockService := &mockSystemEventLogService{name: "mock1", err: nil} _, _, err := getSystemEventLogRaw(ctx, timeout, []systemEventLogProviders{{name: mockService.name, systemEventLogProvider: mockService}}) assert.Nil(t, err) // Test with a mock SystemEventLogService that returns an error mockService = &mockSystemEventLogService{name: "mock2", err: errors.New("mock error")} _, _, err = getSystemEventLogRaw(ctx, timeout, []systemEventLogProviders{{name: mockService.name, systemEventLogProvider: mockService}}) assert.NotNil(t, err) } func TestGetSystemEventLogRawFromInterfaces(t *testing.T) { ctx := context.Background() timeout := 1 * time.Second // Test with an empty slice _, _, err := GetSystemEventLogRawFromInterfaces(ctx, timeout, []interface{}{}) assert.NotNil(t, err) // Test with a slice containing a non-SystemEventLog object _, _, err = GetSystemEventLogRawFromInterfaces(ctx, timeout, []interface{}{"not a SystemEventLog Service"}) assert.NotNil(t, err) // Test with a slice containing a mock SystemEventLogService that returns nil mockService := &mockSystemEventLogService{name: "mock1"} _, _, err = GetSystemEventLogRawFromInterfaces(ctx, timeout, []interface{}{mockService}) assert.Nil(t, err) } ================================================ FILE: bmc/sol.go ================================================ package bmc import ( "context" "fmt" "time" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) // SOLDeactivator for deactivating SOL sessions on a BMC. type SOLDeactivator interface { DeactivateSOL(ctx context.Context) (err error) } // deactivatorProvider is an internal struct to correlate an implementation/provider and its name type deactivatorProvider struct { name string solDeactivator SOLDeactivator } // deactivateSOL tries all implementations for a successful SOL deactivation func deactivateSOL(ctx context.Context, timeout time.Duration, b []deactivatorProvider) (metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range b { if elem.solDeactivator == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() newErr := elem.solDeactivator.DeactivateSOL(ctx) if newErr != nil { err = multierror.Append(err, errors.WithMessagef(newErr, "provider: %v", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return metadataLocal, nil } } return metadataLocal, multierror.Append(err, errors.New("failed to deactivate SOL session")) } // DeactivateSOLFromInterfaces identifies implementations of the SOLDeactivator interface and passes them to the deactivateSOL() wrapper method. func DeactivateSOLFromInterfaces(ctx context.Context, timeout time.Duration, generic []interface{}) (metadata Metadata, err error) { deactivators := make([]deactivatorProvider, 0) for _, elem := range generic { if elem == nil { continue } temp := deactivatorProvider{name: getProviderName(elem)} switch p := elem.(type) { case SOLDeactivator: temp.solDeactivator = p deactivators = append(deactivators, temp) default: e := fmt.Sprintf("not an SOLDeactivator implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(deactivators) == 0 { return metadata, multierror.Append(err, errors.New("no SOLDeactivator implementations found")) } return deactivateSOL(ctx, timeout, deactivators) } ================================================ FILE: bmc/sol_test.go ================================================ package bmc import ( "context" "errors" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-multierror" ) type solTermTester struct { MakeErrorOut bool } func (r *solTermTester) DeactivateSOL(ctx context.Context) (err error) { if r.MakeErrorOut { return errors.New("SOL deactivation failed") } return nil } func (r *solTermTester) Name() string { return "test provider" } func TestDeactivateSOL(t *testing.T) { testCases := map[string]struct { makeErrorOut bool err error ctxTimeout time.Duration }{ "success": {makeErrorOut: false}, "error": {makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider: SOL deactivation failed"), errors.New("failed to deactivate SOL session")}}}, "error context timeout": {makeErrorOut: false, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := solTermTester{MakeErrorOut: tc.makeErrorOut} if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() _, err := deactivateSOL(ctx, 0, []deactivatorProvider{{"test provider", &testImplementation}}) var diff string if err != nil && tc.err != nil { diff = cmp.Diff(err.Error(), tc.err.Error()) } else { diff = cmp.Diff(err, tc.err) } if diff != "" { t.Fatal(diff) } }) } } func TestDeactivateSOLFromInterfaces(t *testing.T) { testCases := map[string]struct { err error badImplementation bool withName bool }{ "success": {}, "success with metadata": {withName: true}, "no implementations found": {badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not an SOLDeactivator implementation: *struct {}"), errors.New("no SOLDeactivator implementations found")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := solTermTester{} generic = []interface{}{&testImplementation} } metadata, err := DeactivateSOLFromInterfaces(context.Background(), 0, generic) var diff string if err != nil && tc.err != nil { diff = cmp.Diff(err.Error(), tc.err.Error()) } else { diff = cmp.Diff(err, tc.err) } if diff != "" { t.Fatal(diff) } if tc.withName { if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { t.Fatal(diff) } } }) } } ================================================ FILE: bmc/user.go ================================================ package bmc import ( "context" "errors" "fmt" "time" "github.com/hashicorp/go-multierror" ) // UserCreator creates a user on a BMC type UserCreator interface { UserCreate(ctx context.Context, user, pass, role string) (ok bool, err error) } // UserUpdater updates a user on a BMC type UserUpdater interface { UserUpdate(ctx context.Context, user, pass, role string) (ok bool, err error) } // UserDeleter deletes a user on a BMC type UserDeleter interface { UserDelete(ctx context.Context, user string) (ok bool, err error) } // UserReader lists all users on a BMC type UserReader interface { UserRead(ctx context.Context) (users []map[string]string, err error) } // userProviders is an internal struct used to correlate an implementation/provider with its name type userProviders struct { name string userCreator UserCreator userUpdater UserUpdater userDeleter UserDeleter userReader UserReader } // createUser creates a user using the passed in implementation func createUser(ctx context.Context, timeout time.Duration, user, pass, role string, u []userProviders) (ok bool, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range u { if elem.userCreator == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return false, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ok, createErr := elem.userCreator.UserCreate(ctx, user, pass, role) if createErr != nil { err = multierror.Append(err, createErr) continue } if !ok { err = multierror.Append(err, errors.New("failed to create user")) continue } metadataLocal.SuccessfulProvider = elem.name return ok, metadataLocal, nil } } return ok, metadataLocal, multierror.Append(err, errors.New("failed to create user")) } // CreateUsersFromInterfaces identifies implementations of the UserCreator interface and passes them to the createUser() wrapper method. func CreateUserFromInterfaces(ctx context.Context, timeout time.Duration, user, pass, role string, generic []interface{}) (ok bool, metadata Metadata, err error) { userCreators := make([]userProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := userProviders{name: getProviderName(elem)} switch u := elem.(type) { case UserCreator: temp.userCreator = u userCreators = append(userCreators, temp) default: e := fmt.Sprintf("not a UserCreator implementation: %T", u) err = multierror.Append(err, errors.New(e)) } } if len(userCreators) == 0 { return ok, metadata, multierror.Append(err, errors.New("no UserCreator implementations found")) } return createUser(ctx, timeout, user, pass, role, userCreators) } // updateUser updates a user's settings func updateUser(ctx context.Context, timeout time.Duration, user, pass, role string, u []userProviders) (ok bool, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range u { if elem.userUpdater == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return false, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ok, UpdateErr := elem.userUpdater.UserUpdate(ctx, user, pass, role) if UpdateErr != nil { err = multierror.Append(err, UpdateErr) continue } if !ok { err = multierror.Append(err, errors.New("failed to update user")) continue } metadataLocal.SuccessfulProvider = elem.name return ok, metadataLocal, nil } } return ok, metadataLocal, multierror.Append(err, errors.New("failed to update user")) } // UpdateUsersFromInterfaces identifies implementations of the UserUpdater interface and passes them to the updateUser() wrapper method. func UpdateUserFromInterfaces(ctx context.Context, timeout time.Duration, user, pass, role string, generic []interface{}) (ok bool, metadata Metadata, err error) { userUpdaters := make([]userProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := userProviders{name: getProviderName(elem)} switch u := elem.(type) { case UserUpdater: temp.userUpdater = u userUpdaters = append(userUpdaters, temp) default: e := fmt.Sprintf("not a UserUpdater implementation: %T", u) err = multierror.Append(err, errors.New(e)) } } if len(userUpdaters) == 0 { return ok, metadata, multierror.Append(err, errors.New("no UserUpdater implementations found")) } return updateUser(ctx, timeout, user, pass, role, userUpdaters) } // deleteUser deletes a user from a BMC func deleteUser(ctx context.Context, timeout time.Duration, user string, u []userProviders) (ok bool, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range u { if elem.userDeleter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return false, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ok, deleteErr := elem.userDeleter.UserDelete(ctx, user) if deleteErr != nil { err = multierror.Append(err, deleteErr) continue } if !ok { err = multierror.Append(err, errors.New("failed to delete user")) continue } metadataLocal.SuccessfulProvider = elem.name return ok, metadataLocal, nil } } return ok, metadataLocal, multierror.Append(err, errors.New("failed to delete user")) } // DeleteUsersFromInterfaces identifies implementations of the UserDeleter interface and passes them to the deleteUser() wrapper method. func DeleteUserFromInterfaces(ctx context.Context, timeout time.Duration, user string, generic []interface{}) (ok bool, metadata Metadata, err error) { userDeleters := make([]userProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := userProviders{name: getProviderName(elem)} switch u := elem.(type) { case UserDeleter: temp.userDeleter = u userDeleters = append(userDeleters, temp) default: e := fmt.Sprintf("not a UserDeleter implementation: %T", u) err = multierror.Append(err, errors.New(e)) } } if len(userDeleters) == 0 { return ok, metadata, multierror.Append(err, errors.New("no UserDeleter implementations found")) } return deleteUser(ctx, timeout, user, userDeleters) } // readUsers returns all users from a BMC func readUsers(ctx context.Context, timeout time.Duration, u []userProviders) (users []map[string]string, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range u { if elem.userReader == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return users, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() users, readErr := elem.userReader.UserRead(ctx) if readErr != nil { err = multierror.Append(err, readErr) continue } metadataLocal.SuccessfulProvider = elem.name return users, metadataLocal, nil } } return users, metadataLocal, multierror.Append(err, errors.New("failed to read users")) } // ReadUsersFromInterfaces identifies implementations of the UserReader interface and passes them to the readUsers() wrapper method. func ReadUsersFromInterfaces(ctx context.Context, timeout time.Duration, generic []interface{}) (users []map[string]string, metadata Metadata, err error) { userReaders := make([]userProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := userProviders{name: getProviderName(elem)} switch u := elem.(type) { case UserReader: temp.userReader = u userReaders = append(userReaders, temp) default: e := fmt.Sprintf("not a UserReader implementation: %T", u) err = multierror.Append(errors.New(e)) } } if len(userReaders) == 0 { return users, metadata, multierror.Append(err, errors.New("no UserReader implementations found")) } return readUsers(ctx, timeout, userReaders) } ================================================ FILE: bmc/user_test.go ================================================ package bmc import ( "context" "errors" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-multierror" ) type userTester struct { MakeNotOK bool MakeErrorOut bool } func (p *userTester) UserCreate(ctx context.Context, user, pass, role string) (ok bool, err error) { if p.MakeErrorOut { return ok, errors.New("create user failed") } if p.MakeNotOK { return false, nil } return true, nil } func (p *userTester) UserUpdate(ctx context.Context, user, pass, role string) (ok bool, err error) { if p.MakeErrorOut { return ok, errors.New("update user failed") } if p.MakeNotOK { return false, nil } return true, nil } func (p *userTester) UserDelete(ctx context.Context, user string) (ok bool, err error) { if p.MakeErrorOut { return ok, errors.New("delete user failed") } if p.MakeNotOK { return false, nil } return true, nil } func (p *userTester) UserRead(ctx context.Context) (users []map[string]string, err error) { if p.MakeErrorOut { return users, errors.New("read users failed") } users = []map[string]string{ { "Auth": "true", "Callin": "true", "ID": "2", "Link": "false", "Name": "ADMIN", }, } return users, nil } func (p *userTester) Name() string { return "test provider" } func TestUserCreate(t *testing.T) { testCases := map[string]struct { makeErrorOut bool makeNotOk bool want bool err error ctxTimeout time.Duration }{ "success": {want: true}, "not ok return": {want: false, makeNotOk: true, err: &multierror.Error{Errors: []error{errors.New("failed to create user"), errors.New("failed to create user")}}}, "error": {makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("create user failed"), errors.New("failed to create user")}}}, "error context timeout": {makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := userTester{MakeErrorOut: tc.makeErrorOut, MakeNotOK: tc.makeNotOk} expectedResult := tc.want user := "ADMIN" pass := "ADMIN" role := "admin" if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() result, _, err := createUser(ctx, 0, user, pass, role, []userProviders{{"", &testImplementation, nil, nil, nil}}) if err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } }) } } func TestCreateUserFromInterfaces(t *testing.T) { testCases := map[string]struct { err error badImplementation bool want bool withMetadata bool }{ "success": {want: true}, "success with metadata": {want: true, withMetadata: true}, "no implementations found": {badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a UserCreator implementation: *struct {}"), errors.New("no UserCreator implementations found")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := userTester{} generic = []interface{}{&testImplementation} } expectedResult := tc.want user := "ADMIN" pass := "ADMIN" role := "admin" result, metadata, err := CreateUserFromInterfaces(context.Background(), 0, user, pass, role, generic) if err != nil { if tc.err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } if tc.withMetadata { if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { t.Logf("%+v", metadata) t.Fatal(diff) } } }) } } func TestUpdateUser(t *testing.T) { testCases := map[string]struct { makeErrorOut bool makeNotOk bool want bool err error ctxTimeout time.Duration }{ "success": {want: true}, "not ok return": {want: false, makeNotOk: true, err: &multierror.Error{Errors: []error{errors.New("failed to update user"), errors.New("failed to update user")}}}, "error": {makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("update user failed"), errors.New("failed to update user")}}}, "error context timeout": {makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := userTester{MakeErrorOut: tc.makeErrorOut, MakeNotOK: tc.makeNotOk} expectedResult := tc.want user := "ADMIN" pass := "ADMIN" role := "admin" if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() result, _, err := updateUser(ctx, 0, user, pass, role, []userProviders{{"", nil, &testImplementation, nil, nil}}) if err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } }) } } func TestUpdateUserFromInterfaces(t *testing.T) { testCases := map[string]struct { err error badImplementation bool want bool withMetadata bool }{ "success": {want: true}, "success with metadata": {want: true, withMetadata: true}, "no implementations found": {badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a UserUpdater implementation: *struct {}"), errors.New("no UserUpdater implementations found")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := userTester{} generic = []interface{}{&testImplementation} } expectedResult := tc.want user := "ADMIN" pass := "ADMIN" role := "admin" result, metadata, err := UpdateUserFromInterfaces(context.Background(), 0, user, pass, role, generic) if err != nil { if tc.err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } if tc.withMetadata { if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { t.Fatal(diff) } } }) } } func TestDeleteUser(t *testing.T) { testCases := map[string]struct { makeErrorOut bool makeNotOk bool want bool err error ctxTimeout time.Duration }{ "success": {want: true}, "not ok return": {want: false, makeNotOk: true, err: &multierror.Error{Errors: []error{errors.New("failed to delete user"), errors.New("failed to delete user")}}}, "error": {makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("delete user failed"), errors.New("failed to delete user")}}}, "error context timeout": {makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := userTester{MakeErrorOut: tc.makeErrorOut, MakeNotOK: tc.makeNotOk} expectedResult := tc.want user := "ADMIN" if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() result, _, err := deleteUser(ctx, 0, user, []userProviders{{"", nil, nil, &testImplementation, nil}}) if err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } }) } } func TestDeleteUserFromInterfaces(t *testing.T) { testCases := map[string]struct { err error badImplementation bool want bool withMetadata bool }{ "success": {want: true}, "success with metadata": {want: true, withMetadata: true}, "no implementations found": {badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a UserDeleter implementation: *struct {}"), errors.New("no UserDeleter implementations found")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := userTester{} generic = []interface{}{&testImplementation} } expectedResult := tc.want user := "ADMIN" result, metadata, err := DeleteUserFromInterfaces(context.Background(), 0, user, generic) if err != nil { if tc.err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } if tc.withMetadata { if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { t.Fatal(diff) } } }) } } func TestReadUsers(t *testing.T) { testCases := map[string]struct { makeErrorOut bool want bool err error ctxTimeout time.Duration }{ "success": {want: true}, "not ok return": {want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("read users failed"), errors.New("failed to read users")}}}, "error context timeout": {want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, } users := []map[string]string{ { "Auth": "true", "Callin": "true", "ID": "2", "Link": "false", "Name": "ADMIN", }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := userTester{MakeErrorOut: tc.makeErrorOut} expectedResult := users if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() result, _, err := readUsers(ctx, 0, []userProviders{{"", nil, nil, nil, &testImplementation}}) if err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } }) } } func TestReadUsersFromInterfaces(t *testing.T) { testCases := map[string]struct { err error badImplementation bool want bool withMetadata bool }{ "success": {want: true}, "success with metadata": {want: true, withMetadata: true}, "no implementations found": {badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a UserReader implementation: *struct {}"), errors.New("no UserReader implementations found")}}}, } users := []map[string]string{ { "Auth": "true", "Callin": "true", "ID": "2", "Link": "false", "Name": "ADMIN", }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := userTester{} generic = []interface{}{&testImplementation} } expectedResult := users result, metadata, err := ReadUsersFromInterfaces(context.Background(), 0, generic) if err != nil { if tc.err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } if tc.withMetadata { if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { t.Fatal(diff) } } }) } } ================================================ FILE: bmc/virtual_media.go ================================================ package bmc import ( "context" "fmt" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" ) // VirtualMediaSetter controls the virtual media attached to a machine type VirtualMediaSetter interface { SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (ok bool, err error) } // VirtualMediaProviders is an internal struct to correlate an implementation/provider and its name type virtualMediaProviders struct { name string virtualMediaSetter VirtualMediaSetter } // setVirtualMedia sets the virtual media. func setVirtualMedia(ctx context.Context, kind string, mediaURL string, b []virtualMediaProviders) (ok bool, metadata Metadata, err error) { var metadataLocal Metadata for _, elem := range b { if elem.virtualMediaSetter == nil { continue } select { case <-ctx.Done(): err = multierror.Append(err, ctx.Err()) return false, metadata, err default: metadataLocal.ProvidersAttempted = append(metadataLocal.ProvidersAttempted, elem.name) ok, setErr := elem.virtualMediaSetter.SetVirtualMedia(ctx, kind, mediaURL) if setErr != nil { err = multierror.Append(err, errors.WithMessagef(setErr, "provider: %v", elem.name)) continue } if !ok { err = multierror.Append(err, fmt.Errorf("provider: %v, failed to set virtual media", elem.name)) continue } metadataLocal.SuccessfulProvider = elem.name return ok, metadataLocal, nil } } return ok, metadataLocal, multierror.Append(err, errors.New("failed to set virtual media")) } // SetVirtualMediaFromInterfaces identifies implementations of the virtualMediaSetter interface and passes the found implementations to the setVirtualMedia() wrapper func SetVirtualMediaFromInterfaces(ctx context.Context, kind string, mediaURL string, generic []interface{}) (ok bool, metadata Metadata, err error) { bdSetters := make([]virtualMediaProviders, 0) for _, elem := range generic { if elem == nil { continue } temp := virtualMediaProviders{name: getProviderName(elem)} switch p := elem.(type) { case VirtualMediaSetter: temp.virtualMediaSetter = p bdSetters = append(bdSetters, temp) default: e := fmt.Sprintf("not a VirtualMediaSetter implementation: %T", p) err = multierror.Append(err, errors.New(e)) } } if len(bdSetters) == 0 { return ok, metadata, multierror.Append(err, errors.New("no VirtualMediaSetter implementations found")) } return setVirtualMedia(ctx, kind, mediaURL, bdSetters) } ================================================ FILE: bmc/virtual_media_test.go ================================================ package bmc import ( "context" "errors" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-multierror" ) type virtualMediaTester struct { MakeNotOK bool MakeErrorOut bool } func (r *virtualMediaTester) SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (ok bool, err error) { if r.MakeErrorOut { return ok, errors.New("setting virtual media failed") } if r.MakeNotOK { return false, nil } return true, nil } func (r *virtualMediaTester) Name() string { return "test provider" } func TestSetVirtualMedia(t *testing.T) { testCases := map[string]struct { kind string mediaURL string makeErrorOut bool makeNotOk bool want bool err error ctxTimeout time.Duration }{ "success": {kind: "cdrom", mediaURL: "example.com/some.iso", want: true}, "not ok return": {kind: "cdrom", mediaURL: "example.com/some.iso", want: false, makeNotOk: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider, failed to set virtual media"), errors.New("failed to set virtual media")}}}, "error": {kind: "cdrom", mediaURL: "example.com/some.iso", want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("provider: test provider: setting virtual media failed"), errors.New("failed to set virtual media")}}}, "error context timeout": {kind: "cdrom", mediaURL: "example.com/some.iso", want: false, makeErrorOut: true, err: &multierror.Error{Errors: []error{errors.New("context deadline exceeded")}}, ctxTimeout: time.Nanosecond * 1}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { testImplementation := virtualMediaTester{MakeErrorOut: tc.makeErrorOut, MakeNotOK: tc.makeNotOk} expectedResult := tc.want if tc.ctxTimeout == 0 { tc.ctxTimeout = time.Second * 3 } ctx, cancel := context.WithTimeout(context.Background(), tc.ctxTimeout) defer cancel() result, _, err := setVirtualMedia(ctx, tc.kind, tc.mediaURL, []virtualMediaProviders{{"test provider", &testImplementation}}) if err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } }) } } func TestSetVirtualMediaFromInterfaces(t *testing.T) { testCases := map[string]struct { kind string mediaURL string err error badImplementation bool want bool withName bool }{ "success": {kind: "cdrom", mediaURL: "example.com/some.iso", want: true}, "success with metadata": {kind: "cdrom", mediaURL: "example.com/some.iso", want: true, withName: true}, "no implementations found": {kind: "cdrom", mediaURL: "example.com/some.iso", want: false, badImplementation: true, err: &multierror.Error{Errors: []error{errors.New("not a VirtualMediaSetter implementation: *struct {}"), errors.New("no VirtualMediaSetter implementations found")}}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var generic []interface{} if tc.badImplementation { badImplementation := struct{}{} generic = []interface{}{&badImplementation} } else { testImplementation := virtualMediaTester{} generic = []interface{}{&testImplementation} } expectedResult := tc.want result, metadata, err := SetVirtualMediaFromInterfaces(context.Background(), tc.kind, tc.mediaURL, generic) if err != nil { if tc.err != nil { diff := cmp.Diff(err.Error(), tc.err.Error()) if diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else { diff := cmp.Diff(result, expectedResult) if diff != "" { t.Fatal(diff) } } if tc.withName { if diff := cmp.Diff(metadata.SuccessfulProvider, "test provider"); diff != "" { t.Fatal(diff) } } }) } } ================================================ FILE: client.go ================================================ // Package bmclib client.go is intended to be the main public API. // Its purpose is to make interacting with bmclib as friendly as possible. package bmclib import ( "context" "fmt" "io" "net/http" "os" "strings" "sync" "time" "dario.cat/mergo" "github.com/bmc-toolbox/bmclib/v2/bmc" "github.com/bmc-toolbox/bmclib/v2/constants" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/bmc-toolbox/bmclib/v2/providers/asrockrack" "github.com/bmc-toolbox/bmclib/v2/providers/dell" "github.com/bmc-toolbox/bmclib/v2/providers/homeassistant" "github.com/bmc-toolbox/bmclib/v2/providers/intelamt" "github.com/bmc-toolbox/bmclib/v2/providers/ipmitool" "github.com/bmc-toolbox/bmclib/v2/providers/openbmc" "github.com/bmc-toolbox/bmclib/v2/providers/redfish" "github.com/bmc-toolbox/bmclib/v2/providers/rpc" "github.com/bmc-toolbox/bmclib/v2/providers/supermicro" "github.com/bmc-toolbox/common" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" "go.opentelemetry.io/otel/attribute" oteltrace "go.opentelemetry.io/otel/trace" tracenoop "go.opentelemetry.io/otel/trace/noop" ) const ( // default connection timeout defaultConnectTimeout = 30 * time.Second pkgName = "github.com/bmc-toolbox/bmclib" ) // Client for BMC interactions type Client struct { Auth Auth Logger logr.Logger Registry *registrar.Registry httpClient *http.Client httpClientSetupFuncs []func(*http.Client) mdLock *sync.Mutex metadata *bmc.Metadata perProviderTimeout func(context.Context) time.Duration oneTimeRegistry *registrar.Registry oneTimeRegistryEnabled bool providerConfig providerConfig traceprovider oteltrace.TracerProvider } // Auth details for connecting to a BMC type Auth struct { Host string User string Pass string } // providerConfig contains per provider specific configuration. type providerConfig struct { ipmitool ipmitool.Config asrock asrockrack.Config gofish redfish.Config intelamt intelamt.Config dell dell.Config supermicro supermicro.Config rpc rpc.Provider openbmc openbmc.Config homeassistant homeassistant.Config } // NewClient returns a new Client struct func NewClient(host, user, pass string, opts ...Option) *Client { defaultClient := &Client{ Logger: logr.Discard(), Registry: registrar.NewRegistry(), oneTimeRegistryEnabled: false, oneTimeRegistry: registrar.NewRegistry(), httpClient: httpclient.Build(), traceprovider: tracenoop.NewTracerProvider(), providerConfig: providerConfig{ ipmitool: ipmitool.Config{ Port: "623", }, asrock: asrockrack.Config{ Port: "443", }, gofish: redfish.Config{ Port: "443", VersionsNotCompatible: []string{}, }, intelamt: intelamt.Config{ HostScheme: "http", Port: 16992, }, dell: dell.Config{ Port: "443", VersionsNotCompatible: []string{}, }, supermicro: supermicro.Config{ Port: "443", }, rpc: rpc.Provider{}, openbmc: openbmc.Config{ Port: "443", }, homeassistant: homeassistant.Config{}, }, } for _, opt := range opts { opt(defaultClient) } for _, setupFunc := range defaultClient.httpClientSetupFuncs { setupFunc(defaultClient.httpClient) } defaultClient.Registry.Logger = defaultClient.Logger defaultClient.Auth.Host = host defaultClient.Auth.User = user defaultClient.Auth.Pass = pass // len of 0 means that no Registry, with any registered providers, was passed in. if len(defaultClient.Registry.Drivers) == 0 { defaultClient.registerProviders() } defaultClient.mdLock = &sync.Mutex{} if defaultClient.perProviderTimeout == nil { defaultClient.perProviderTimeout = defaultClient.defaultTimeout } return defaultClient } func (c *Client) defaultTimeout(ctx context.Context) time.Duration { deadline, ok := ctx.Deadline() if !ok { return defaultConnectTimeout } l := len(c.registry().Drivers) if l == 0 { return time.Until(deadline) } return time.Until(deadline) / time.Duration(l) } func (c *Client) registerHomeAssistantProvider() error { driverHA := homeassistant.New(c.Auth.Host, c.Auth.Pass) c.providerConfig.homeassistant.Logger = c.Logger httpClient := *c.httpClient httpClient.Transport = c.httpClient.Transport.(*http.Transport).Clone() c.providerConfig.homeassistant.HTTPClient = &httpClient if err := mergo.Merge(driverHA, c.providerConfig.homeassistant, mergo.WithOverride); err != nil { return fmt.Errorf("failed to merge user specified homeassistant config with the config defaults, homeassistant provider not available: %w", err) } c.Registry.Register(homeassistant.ProviderName, homeassistant.ProviderProtocol, homeassistant.Features, nil, driverHA) return nil } func (c *Client) registerRPCProvider() error { driverRPC := rpc.New(c.providerConfig.rpc.ConsumerURL, c.Auth.Host, c.providerConfig.rpc.Opts.HMAC.Secrets) c.providerConfig.rpc.Logger = c.Logger httpClient := *c.httpClient httpClient.Transport = c.httpClient.Transport.(*http.Transport).Clone() c.providerConfig.rpc.HTTPClient = &httpClient if err := mergo.Merge(driverRPC, c.providerConfig.rpc, mergo.WithOverride, mergo.WithTransformers(&rpc.Provider{})); err != nil { return fmt.Errorf("failed to merge user specified rpc config with the config defaults, rpc provider not available: %w", err) } c.Registry.Register(rpc.ProviderName, rpc.ProviderProtocol, rpc.Features, nil, driverRPC) return nil } // register ipmitool provider func (c *Client) registerIPMIProvider() error { ipmiOpts := []ipmitool.Option{ ipmitool.WithLogger(c.Logger), ipmitool.WithPort(c.providerConfig.ipmitool.Port), ipmitool.WithCipherSuite(c.providerConfig.ipmitool.CipherSuite), ipmitool.WithIpmitoolPath(c.providerConfig.ipmitool.IpmitoolPath), } driverIpmitool, err := ipmitool.New(c.Auth.Host, c.Auth.User, c.Auth.Pass, ipmiOpts...) if err != nil { return err } c.Registry.Register(ipmitool.ProviderName, ipmitool.ProviderProtocol, ipmitool.Features, nil, driverIpmitool) return nil } // register ASRR vendorapi provider func (c *Client) registerASRRProvider() { asrHttpClient := *c.httpClient asrHttpClient.Transport = c.httpClient.Transport.(*http.Transport).Clone() driverAsrockrack := asrockrack.NewWithOptions(c.Auth.Host+":"+c.providerConfig.asrock.Port, c.Auth.User, c.Auth.Pass, c.Logger, asrockrack.WithHTTPClient(&asrHttpClient)) c.Registry.Register(asrockrack.ProviderName, asrockrack.ProviderProtocol, asrockrack.Features, nil, driverAsrockrack) } // register gofish provider func (c *Client) registerGofishProvider() { gfHttpClient := *c.httpClient gfHttpClient.Transport = c.httpClient.Transport.(*http.Transport).Clone() gofishOpts := []redfish.Option{ redfish.WithHttpClient(&gfHttpClient), redfish.WithVersionsNotCompatible(c.providerConfig.gofish.VersionsNotCompatible), redfish.WithUseBasicAuth(c.providerConfig.gofish.UseBasicAuth), redfish.WithPort(c.providerConfig.gofish.Port), redfish.WithEtagMatchDisabled(c.providerConfig.gofish.DisableEtagMatch), redfish.WithSystemName(c.providerConfig.gofish.SystemName), } driverGoFish := redfish.New(c.Auth.Host, c.Auth.User, c.Auth.Pass, c.Logger, gofishOpts...) c.Registry.Register(redfish.ProviderName, redfish.ProviderProtocol, redfish.Features, nil, driverGoFish) } // register Intel AMT provider func (c *Client) registerIntelAMTProvider() { iamtOpts := []intelamt.Option{ intelamt.WithLogger(c.Logger), intelamt.WithHostScheme(c.providerConfig.intelamt.HostScheme), intelamt.WithPort(c.providerConfig.intelamt.Port), } driverAMT := intelamt.New(c.Auth.Host, c.Auth.User, c.Auth.Pass, iamtOpts...) c.Registry.Register(intelamt.ProviderName, intelamt.ProviderProtocol, intelamt.Features, nil, driverAMT) } // register Dell gofish provider func (c *Client) registerDellProvider() { dellGofishHttpClient := *c.httpClient //dellGofishHttpClient.Transport = c.httpClient.Transport.(*http.Transport).Clone() dellGofishOpts := []dell.Option{ dell.WithHttpClient(&dellGofishHttpClient), dell.WithVersionsNotCompatible(c.providerConfig.dell.VersionsNotCompatible), dell.WithUseBasicAuth(c.providerConfig.dell.UseBasicAuth), dell.WithPort(c.providerConfig.dell.Port), } driverGoFishDell := dell.New(c.Auth.Host, c.Auth.User, c.Auth.Pass, c.Logger, dellGofishOpts...) c.Registry.Register(dell.ProviderName, redfish.ProviderProtocol, dell.Features, nil, driverGoFishDell) } // register supermicro vendorapi provider func (c *Client) registerSupermicroProvider() { smcHttpClient := *c.httpClient smcHttpClient.Transport = c.httpClient.Transport.(*http.Transport).Clone() driverSupermicro := supermicro.NewClient( c.Auth.Host, c.Auth.User, c.Auth.Pass, c.Logger, supermicro.WithHttpClient(&smcHttpClient), supermicro.WithPort(c.providerConfig.supermicro.Port), ) c.Registry.Register(supermicro.ProviderName, supermicro.ProviderProtocol, supermicro.Features, nil, driverSupermicro) } func (c *Client) registerOpenBMCProvider() { httpClient := *c.httpClient httpClient.Transport = c.httpClient.Transport.(*http.Transport).Clone() driver := openbmc.New( c.Auth.Host, c.Auth.User, c.Auth.Pass, c.Logger, openbmc.WithHttpClient(&httpClient), openbmc.WithPort(c.providerConfig.openbmc.Port), ) c.Registry.Register(openbmc.ProviderName, openbmc.ProviderProtocol, openbmc.Features, nil, driver) } func (c *Client) registerProviders() { // register the homeassistant provider, if options for it were provided if c.providerConfig.homeassistant.SwitchEntityID != "" { // when the homeassistant provider is to be used, we won't register any other providers. err := c.registerHomeAssistantProvider() if err == nil { c.Logger.Info("note: with the homeassistant provider registered, no other providers will be registered and available") return } c.Logger.Info("failed to register homeassistant provider, falling back to registering all other providers", "error", err.Error()) } // register the rpc provider // without the consumer URL there is no way to send RPC requests. if c.providerConfig.rpc.ConsumerURL != "" { // when the rpc provider is to be used, we won't register any other providers. err := c.registerRPCProvider() if err == nil { c.Logger.Info("note: with the rpc provider registered, no other providers will be registered and available") return } c.Logger.Info("failed to register rpc provider, falling back to registering all other providers", "error", err.Error()) } if err := c.registerIPMIProvider(); err != nil { c.Logger.Info("ipmitool provider not available", "error", err.Error()) } c.registerASRRProvider() c.registerGofishProvider() c.registerIntelAMTProvider() c.registerDellProvider() c.registerSupermicroProvider() c.registerOpenBMCProvider() } // GetMetadata returns the metadata that is populated after each BMC function/method call func (c *Client) GetMetadata() bmc.Metadata { if c.metadata != nil { return *c.metadata } return bmc.Metadata{} } // setMetadata wraps setting metadata with a mutex for cases where users are // making calls to multiple *Client.X functions/methods across goroutines func (c *Client) setMetadata(metadata bmc.Metadata) { // a mutex is created with the NewClient func, in the case // where a user doesn't call NewClient we handle by checking if // the mutex is nil if c.mdLock != nil { c.mdLock.Lock() defer c.mdLock.Unlock() } c.metadata = &metadata } // registry will return the oneTimeRegistry if the oneTimeRegistryEnabled is true. func (c *Client) registry() *registrar.Registry { if c.oneTimeRegistryEnabled { c.oneTimeRegistryEnabled = false return c.oneTimeRegistry } return c.Registry } func (c *Client) RegisterSpanAttributes(m bmc.Metadata, span oteltrace.Span) { span.SetAttributes(attribute.String("host", c.Auth.Host)) span.SetAttributes(attribute.String("successful-provider", m.SuccessfulProvider)) span.SetAttributes( attribute.String("successful-open-conns", strings.Join(m.SuccessfulOpenConns, ",")), ) span.SetAttributes( attribute.String("successful-close-conns", strings.Join(m.SuccessfulCloseConns, ",")), ) span.SetAttributes( attribute.String("attempted-providers", strings.Join(m.ProvidersAttempted, ",")), ) for p, e := range m.FailedProviderDetail { span.SetAttributes( attribute.String("provider-errs-"+p, e), ) } } // Open calls the OpenConnectionFromInterfaces library function // Any providers/drivers that do not successfully connect are removed // from the client.Registry.Drivers. If client.Registry.Drivers ends up // being empty then we error. func (c *Client) Open(ctx context.Context) error { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "Open") defer span.End() ifs, metadata, err := bmc.OpenConnectionFromInterfaces(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) metadata.RegisterSpanAttributes(c.Auth.Host, span) defer c.setMetadata(metadata) if err != nil { return err } var reg registrar.Drivers for _, elem := range c.Registry.Drivers { for _, em := range ifs { if em == elem.DriverInterface { elem.DriverInterface = em reg = append(reg, elem) } } } c.Registry.Drivers = reg return nil } // Close pass through to library function func (c *Client) Close(ctx context.Context) (err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "Close") defer span.End() // Generally, we always want the close function to run. // We don't want a context timeout or cancellation to prevent this. // But because the current model is to pass just a single context to all // functions, we need to create a new context here allowing closing connections. // This is a short term solution, and we should consider a better/more holistic model. if err := ctx.Err(); err != nil { var done context.CancelFunc ctx, done = context.WithTimeout(context.Background(), defaultConnectTimeout) defer done() } metadata, err := bmc.CloseConnectionFromInterfaces(ctx, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return err } // FilterForCompatible removes any drivers/providers that are not compatible. It wraps the // Client.Registry.FilterForCompatible func in order to provide a per provider timeout. func (c *Client) FilterForCompatible(ctx context.Context) { perProviderTimeout, cancel := context.WithTimeout(ctx, c.perProviderTimeout(ctx)) defer cancel() reg := c.registry().FilterForCompatible(perProviderTimeout) c.Registry.Drivers = reg } // GetPowerState pass through to library function func (c *Client) GetPowerState(ctx context.Context) (state string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "GetPowerState") defer span.End() state, metadata, err := bmc.GetPowerStateFromInterfaces(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return state, err } // SetPowerState pass through to library function func (c *Client) SetPowerState(ctx context.Context, state string) (ok bool, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "SetPowerState") defer span.End() ok, metadata, err := bmc.SetPowerStateFromInterfaces(ctx, c.perProviderTimeout(ctx), state, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return ok, err } // CreateUser pass through to library function func (c *Client) CreateUser(ctx context.Context, user, pass, role string) (ok bool, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "CreateUser") defer span.End() ok, metadata, err := bmc.CreateUserFromInterfaces(ctx, c.perProviderTimeout(ctx), user, pass, role, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return ok, err } // UpdateUser pass through to library function func (c *Client) UpdateUser(ctx context.Context, user, pass, role string) (ok bool, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "UpdateUser") defer span.End() ok, metadata, err := bmc.UpdateUserFromInterfaces(ctx, c.perProviderTimeout(ctx), user, pass, role, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return ok, err } // DeleteUser pass through to library function func (c *Client) DeleteUser(ctx context.Context, user string) (ok bool, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "DeleteUser") defer span.End() ok, metadata, err := bmc.DeleteUserFromInterfaces(ctx, c.perProviderTimeout(ctx), user, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return ok, err } // ReadUsers pass through to library function func (c *Client) ReadUsers(ctx context.Context) (users []map[string]string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "ReadUsers") defer span.End() users, metadata, err := bmc.ReadUsersFromInterfaces(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return users, err } // GetBootDeviceOverride pass through to library function func (c *Client) GetBootDeviceOverride(ctx context.Context) (override bmc.BootDeviceOverride, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "GetBootDeviceOverride") defer span.End() override, metadata, err := bmc.GetBootDeviceOverrideFromInterface(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) c.setMetadata(metadata) return override, err } // SetBootDevice pass through to library function func (c *Client) SetBootDevice(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "SetBootDevice") defer span.End() ok, metadata, err := bmc.SetBootDeviceFromInterfaces(ctx, c.perProviderTimeout(ctx), bootDevice, setPersistent, efiBoot, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return ok, err } // SetVirtualMedia controls the virtual media simulated by the BMC as being connected to the // server. Specifically, the method ejects any currently attached virtual media, and then if // mediaURL isn't empty, attaches a virtual media device of type kind whose contents are // streamed from the indicated URL. func (c *Client) SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (ok bool, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "SetVirtualMedia") defer span.End() ok, metadata, err := bmc.SetVirtualMediaFromInterfaces(ctx, kind, mediaURL, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return ok, err } // ResetBMC pass through to library function func (c *Client) ResetBMC(ctx context.Context, resetType string) (ok bool, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "ResetBMC") defer span.End() ok, metadata, err := bmc.ResetBMCFromInterfaces(ctx, c.perProviderTimeout(ctx), resetType, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return ok, err } // DeactivateSOL pass through library function to deactivate active SOL sessions func (c *Client) DeactivateSOL(ctx context.Context) (err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "DeactivateSOL") defer span.End() metadata, err := bmc.DeactivateSOLFromInterfaces(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) c.setMetadata(metadata) return err } // Inventory pass through library function to collect hardware and firmware inventory func (c *Client) Inventory(ctx context.Context) (device *common.Device, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "Inventory") defer span.End() device, metadata, err := bmc.GetInventoryFromInterfaces(ctx, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) return device, err } func (c *Client) GetBiosConfiguration(ctx context.Context) (biosConfig map[string]string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "GetBiosConfiguration") defer span.End() biosConfig, metadata, err := bmc.GetBiosConfigurationInterfaces(ctx, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return biosConfig, err } func (c *Client) SetBiosConfiguration(ctx context.Context, biosConfig map[string]string) (err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "SetBiosConfiguration") defer span.End() metadata, err := bmc.SetBiosConfigurationInterfaces(ctx, c.registry().GetDriverInterfaces(), biosConfig) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return err } func (c *Client) SetBiosConfigurationFromFile(ctx context.Context, cfg string) (err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "SetBiosConfigurationFromFile") defer span.End() metadata, err := bmc.SetBiosConfigurationFromFileInterfaces(ctx, c.registry().GetDriverInterfaces(), cfg) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return err } func (c *Client) ResetBiosConfiguration(ctx context.Context) (err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "ResetBiosConfiguration") defer span.End() metadata, err := bmc.ResetBiosConfigurationInterfaces(ctx, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return err } // FirmwareInstall pass through library function to upload firmware and install firmware func (c *Client) FirmwareInstall(ctx context.Context, component string, operationApplyTime string, forceInstall bool, reader io.Reader) (taskID string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "FirmwareInstall") defer span.End() taskID, metadata, err := bmc.FirmwareInstallFromInterfaces(ctx, component, operationApplyTime, forceInstall, reader, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return taskID, err } // Note: this interface is to be deprecated in favour of a more generic FirmwareTaskStatus. // // FirmwareInstallStatus pass through library function to check firmware install status func (c *Client) FirmwareInstallStatus(ctx context.Context, installVersion, component, taskID string) (status string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "FirmwareInstallStatus") defer span.End() status, metadata, err := bmc.FirmwareInstallStatusFromInterfaces(ctx, installVersion, component, taskID, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return status, err } // PostCodeGetter pass through library function to return the BIOS/UEFI POST code func (c *Client) PostCode(ctx context.Context) (status string, code int, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "PostCode") defer span.End() status, code, metadata, err := bmc.GetPostCodeInterfaces(ctx, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return status, code, err } func (c *Client) Screenshot(ctx context.Context) (image []byte, fileType string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "Screenshot") defer span.End() image, fileType, metadata, err := bmc.ScreenshotFromInterfaces(ctx, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return image, fileType, err } func (c *Client) ClearSystemEventLog(ctx context.Context) (err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "ClearSystemEventLog") defer span.End() metadata, err := bmc.ClearSystemEventLogFromInterfaces(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return err } func (c *Client) MountFloppyImage(ctx context.Context, image io.Reader) (err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "MountFloppyImage") defer span.End() metadata, err := bmc.MountFloppyImageFromInterfaces(ctx, image, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return err } func (c *Client) UnmountFloppyImage(ctx context.Context) (err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "UnmountFloppyImage") defer span.End() metadata, err := bmc.UnmountFloppyImageFromInterfaces(ctx, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return err } // FirmwareInstallSteps return the order of actions required install firmware for a component. func (c *Client) FirmwareInstallSteps(ctx context.Context, component string) (actions []constants.FirmwareInstallStep, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "FirmwareInstallSteps") defer span.End() status, metadata, err := bmc.FirmwareInstallStepsFromInterfaces(ctx, component, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return status, err } // FirmwareUpload just uploads the firmware for install, it returns a task ID to verify the upload status. func (c *Client) FirmwareUpload(ctx context.Context, component string, file *os.File) (uploadVerifyTaskID string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "FirmwareUpload") defer span.End() uploadVerifyTaskID, metadata, err := bmc.FirmwareUploadFromInterfaces(ctx, component, file, c.Registry.GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return uploadVerifyTaskID, err } // FirmwareTaskStatus pass through library function to check firmware task statuses func (c *Client) FirmwareTaskStatus(ctx context.Context, kind constants.FirmwareInstallStep, component, taskID, installVersion string) (state constants.TaskState, status string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "FirmwareTaskStatus") defer span.End() state, status, metadata, err := bmc.FirmwareTaskStatusFromInterfaces(ctx, kind, component, taskID, installVersion, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return state, status, err } // FirmwareInstallUploaded kicks off firmware install for a firmware uploaded with FirmwareUpload. func (c *Client) FirmwareInstallUploaded(ctx context.Context, component, uploadVerifyTaskID string) (installTaskID string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "FirmwareInstallUploaded") defer span.End() installTaskID, metadata, err := bmc.FirmwareInstallerUploadedFromInterfaces(ctx, component, uploadVerifyTaskID, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return installTaskID, err } func (c *Client) FirmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File) (taskID string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "FirmwareInstallUploadAndInitiate") defer span.End() taskID, metadata, err := bmc.FirmwareInstallUploadAndInitiateFromInterfaces(ctx, component, file, c.registry().GetDriverInterfaces()) c.setMetadata(metadata) metadata.RegisterSpanAttributes(c.Auth.Host, span) return taskID, err } // GetSystemEventLog queries for the SEL and returns the entries in an opinionated format. func (c *Client) GetSystemEventLog(ctx context.Context) (entries bmc.SystemEventLogEntries, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "GetSystemEventLog") defer span.End() entries, metadata, err := bmc.GetSystemEventLogFromInterfaces(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) c.setMetadata(metadata) return entries, err } // GetSystemEventLogRaw queries for the SEL and returns the raw response. func (c *Client) GetSystemEventLogRaw(ctx context.Context) (eventlog string, err error) { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "GetSystemEventLogRaw") defer span.End() eventlog, metadata, err := bmc.GetSystemEventLogRawFromInterfaces(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) c.setMetadata(metadata) return eventlog, err } // SendNMI tells the BMC to issue an NMI to the device func (c *Client) SendNMI(ctx context.Context) error { ctx, span := c.traceprovider.Tracer(pkgName).Start(ctx, "SendNMI") defer span.End() metadata, err := bmc.SendNMIFromInterface(ctx, c.perProviderTimeout(ctx), c.registry().GetDriverInterfaces()) c.setMetadata(metadata) return err } ================================================ FILE: client_test.go ================================================ package bmclib import ( "context" "testing" "time" "github.com/bmc-toolbox/bmclib/v2/logging" "github.com/google/go-cmp/cmp" "github.com/jacobweinstock/registrar" "gopkg.in/go-playground/assert.v1" ) func TestBMC(t *testing.T) { t.Skip("needs ipmitool and real ipmi server") host := "127.0.0.1" user := "admin" pass := "admin" log := logging.DefaultLogger() ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() cl := NewClient(host, user, pass, WithLogger(log), WithPerProviderTimeout(5*time.Second)) if err := cl.Open(ctx); err != nil { t.Logf("%+v", cl.GetMetadata()) t.Fatal(err) } defer cl.Close(ctx) t.Logf("metadata for Open: %+v", cl.GetMetadata()) cl.Registry.Drivers = cl.Registry.PreferDriver("non-existent") state, err := cl.GetPowerState(ctx) if err != nil { t.Fatal(err) } t.Log(state) t.Logf("metadata for GetPowerState: %+v", cl.GetMetadata()) cl.Registry.Drivers = cl.Registry.PreferDriver("ipmitool") state, err = cl.PreferProvider("gofish").GetPowerState(ctx) if err != nil { t.Fatal(err) } t.Log(state) t.Logf("metadata for GetPowerState: %+v", cl.GetMetadata()) users, err := cl.ReadUsers(ctx) if err != nil { t.Fatal(err) } t.Log(users) t.Logf("metadata for ReadUsers: %+v", cl.GetMetadata()) t.Fatal() } func TestWithRedfishVersionsNotCompatible(t *testing.T) { host := "127.0.0.1" user := "ADMIN" pass := "ADMIN" tests := []struct { name string versions []string }{ { "no versions", []string{}, }, { "with versions", []string{"1.2.3", "4.5.6"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cl := NewClient(host, user, pass, WithRedfishVersionsNotCompatible(tt.versions)) assert.Equal(t, tt.versions, cl.providerConfig.gofish.VersionsNotCompatible) }) } } func TestWithRedfishBasicAuth(t *testing.T) { host := "127.0.0.1" user := "ADMIN" pass := "ADMIN" tests := []struct { name string enabled bool }{ { "disabled", false, }, { "enabled", true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var opts []Option if tt.enabled { opts = append(opts, WithRedfishUseBasicAuth(true)) } cl := NewClient(host, user, pass, opts...) assert.Equal(t, tt.enabled, cl.providerConfig.gofish.UseBasicAuth) }) } } func TestWithConnectionTimeout(t *testing.T) { host := "127.0.0.1" user := "ADMIN" pass := "ADMIN" tests := []struct { name string timeout time.Duration }{ { "no connection timeout", 0, }, { "with timeout", 5 * time.Second, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cl := NewClient(host, user, pass, WithPerProviderTimeout(tt.timeout)) assert.Equal(t, tt.timeout, cl.perProviderTimeout(nil)) }) } } func TestDefaultTimeout(t *testing.T) { tests := map[string]struct { ctx context.Context want func(n int) time.Duration }{ "no per provider timeout": { ctx: context.Background(), want: func(n int) time.Duration { return 30 * time.Second }, }, "with per provider timeout": { ctx: func() context.Context { c, d := context.WithTimeout(context.Background(), 5*time.Second) defer d() return c }(), want: func(n int) time.Duration { v := (4999 * time.Millisecond / time.Duration(n)) return v.Round(time.Millisecond) }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { c := NewClient("", "", "") got := c.defaultTimeout(tt.ctx) if !equalWithinErrorMargin(got.Round(time.Millisecond), tt.want(len(c.Registry.Drivers))) { diff := cmp.Diff(got.Round(time.Millisecond), tt.want(len(c.Registry.Drivers))) t.Errorf("unexpected timeout (-want +got):\n%s", diff) } }) } } func equalWithinErrorMargin(a, b time.Duration) bool { return (a - b) < 10*time.Millisecond } type testProvider struct { PName string Powerstate string BootdeviceOK bool Err error } func (t *testProvider) Name() string { if t.PName != "" { return t.PName } return "tester" } func (t *testProvider) Open(ctx context.Context) error { return t.Err } func (t *testProvider) Close(ctx context.Context) error { return t.Err } func (t *testProvider) PowerStateGet(ctx context.Context) (string, error) { return t.Powerstate, t.Err } func (t *testProvider) PowerSet(ctx context.Context, state string) error { return t.Err } func (t *testProvider) BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { return t.BootdeviceOK, t.Err } func registryNames(r []*registrar.Driver) []string { var names []string for _, d := range r { names = append(names, d.Name) } return names } func TestOpenFiltered(t *testing.T) { registry := registrar.NewRegistry() registry.Register("tester1", "tester1", nil, nil, &testProvider{PName: "tester1"}) registry.Register("tester2", "tester2", nil, nil, &testProvider{PName: "tester2"}) registry.Register("tester3", "tester3", nil, nil, &testProvider{PName: "tester3"}) cl := NewClient("", "", "", WithRegistry(registry)) if err := cl.PreferProvider("tester3").Open(context.Background()); err != nil { t.Fatal(err) } defer cl.Close(context.Background()) want := []string{"tester3", "tester1", "tester2"} if diff := cmp.Diff(cl.GetMetadata().ProvidersAttempted, want); diff != "" { t.Errorf("diff: %s", diff) } want = []string{"tester1", "tester2", "tester3"} if diff := cmp.Diff(registryNames(cl.Registry.Drivers), want); diff != "" { t.Errorf("diff: %s", diff) } } ================================================ FILE: constants/constants.go ================================================ package constants type ( // Redfish operation apply time parameter OperationApplyTime string // The FirmwareInstallStep identifies each phase of a firmware install process. FirmwareInstallStep string TaskState string ) const ( // EnvEnableDebug is the const for the environment variable to cause bmclib to dump debugging debugging information. // the valid parameter for this environment variable is 'true' EnvEnableDebug = "DEBUG_BMCLIB" // Vendor constants // HP is the constant that defines the vendor HP HP = "HP" // Dell is the constant that defines the vendor Dell Dell = "Dell" // Supermicro is the constant that defines the vendor Supermicro Supermicro = "Supermicro" // Cloudline is the constant that defines the cloudlines Cloudline = "Cloudline" // Quanta is the contant to identify Quanta hardware Quanta = "Quanta" // Quanta is the contant to identify Intel hardware Intel = "Intel" // Redfish firmware apply at constants // FirmwareApplyImmediate sets the firmware to be installed immediately after upload Immediate OperationApplyTime = "Immediate" //FirmwareApplyOnReset sets the firmware to be install on device power cycle/reset OnReset OperationApplyTime = "OnReset" // FirmwareOnStartUpdateRequest sets the firmware install to begin after the start request has been sent. OnStartUpdateRequest OperationApplyTime = "OnStartUpdateRequest" // TODO: rename FirmwareInstall* task status names to FirmwareTaskState and declare a type. // Firmware install states returned by bmclib provider FirmwareInstallStatus implementations // // The redfish from the redfish spec are exposed as a smaller set of bmclib states for callers // https://www.dmtf.org/sites/default/files/standards/documents/DSP2046_2020.3.pdf // FirmwareInstallInitializing indicates the device is performing init actions to install the update // this covers the redfish states - 'starting', 'downloading' // no action is required from the callers part in this state FirmwareInstallInitializing = "initializing" Initializing TaskState = "initializing" // FirmwareInstallQueued indicates the device has queued the update, but has not started the update task yet // this covers the redfish states - 'pending', 'new' // no action is required from the callers part in this state FirmwareInstallQueued = "queued" Queued TaskState = "queued" // FirmwareInstallRunner indicates the device is installing the update // this covers the redfish states - 'running', 'stopping', 'cancelling' // no action is required from the callers part in this state FirmwareInstallRunning = "running" Running TaskState = "running" // FirmwareInstallComplete indicates the device completed the firmware install // this covers the redfish state - 'complete' FirmwareInstallComplete = "complete" Complete TaskState = "complete" // FirmwareInstallFailed indicates the firmware install failed // this covers the redfish states - 'interrupted', 'killed', 'exception', 'cancelled', 'suspended' FirmwareInstallFailed = "failed" Failed TaskState = "failed" // FirmwareInstallPowerCycleHost indicates the firmware install requires a host power cycle FirmwareInstallPowerCycleHost = "powercycle-host" PowerCycleHost TaskState = "powercycle-host" FirmwareInstallUnknown = "unknown" Unknown TaskState = "unknown" // FirmwareInstallStepUploadInitiateInstall identifies the step to upload _and_ initialize the firmware install. // as part of the same call. FirmwareInstallStepUploadInitiateInstall FirmwareInstallStep = "upload-initiate-install" // FirmwareInstallStepInstallStatus identifies the step to verify the status of the firmware install. FirmwareInstallStepInstallStatus FirmwareInstallStep = "install-status" // FirmwareInstallStepUpload identifies the upload step in the firmware install process. FirmwareInstallStepUpload FirmwareInstallStep = "upload" // FirmwareInstallStepUploadStatus identifies the step to verify the upload status as part of the firmware install status. FirmwareInstallStepUploadStatus FirmwareInstallStep = "upload-status" // FirmwareInstallStepInstallUploaded identifies the step to install firmware uploaded in FirmwareInstallStepUpload. FirmwareInstallStepInstallUploaded FirmwareInstallStep = "install-uploaded" // FirmwareInstallStepPowerOffHost indicates the host requires to be powered off. FirmwareInstallStepPowerOffHost FirmwareInstallStep = "power-off-host" // FirmwareInstallStepResetBMCPostInstall indicates the BMC requires a reset after the install. FirmwareInstallStepResetBMCPostInstall FirmwareInstallStep = "reset-bmc-post-install" // FirmwareInstallStepResetBMCOnInstallFailure indicates the BMC requires a reset if an install fails. FirmwareInstallStepResetBMCOnInstallFailure FirmwareInstallStep = "reset-bmc-on-install-failure" // device BIOS/UEFI POST code bmclib identifiers POSTStateBootINIT = "boot-init/pxe" POSTStateUEFI = "uefi" POSTStateOS = "grub/os" POSTCodeUnknown = "unknown" ) // ListSupportedVendors returns a list of supported vendors func ListSupportedVendors() []string { return []string{HP, Dell, Supermicro} } ================================================ FILE: doc.go ================================================ // Copyright 2022 The bmclib Authors. All rights reserved. // Use of this source code is governed by an Apache that can be found in the LICENSE file. /* Package bmclib abstracts interacting with Baseboard Management controllers. see the examples directory for usage. */ package bmclib ================================================ FILE: errors/errors.go ================================================ package errors import ( "errors" "fmt" ) var ( // ErrLoginFailed is returned when we fail to login to a bmc ErrLoginFailed = errors.New("failed to login") // ErrLogoutFailed is returned when we fail to logout from a bmc ErrLogoutFailed = errors.New("failed to logout") // ErrNotAuthenticated is returned when the session is not active. ErrNotAuthenticated = errors.New("not authenticated") // ErrNon200Response is returned when bmclib recieves an unexpected non-200 status code for a query ErrNon200Response = errors.New("non-200 response returned for the endpoint") // ErrNotImplemented is returned for not implemented methods called ErrNotImplemented = errors.New("this feature hasn't been implemented yet") // ErrRetrievingUserAccounts is returned when bmclib is unable to retrieve user accounts from the BMC ErrRetrievingUserAccounts = errors.New("error retrieving user accounts") // ErrInvalidUserRole is returned when the given user account role is not valid ErrInvalidUserRole = errors.New("invalid user account role") // ErrUserParamsRequired is returned when all the required user parameters are not provided - username, password, role ErrUserParamsRequired = errors.New("username, password and role are required parameters") // ErrUserAccountExists is returned when a user account with the username is already present ErrUserAccountExists = errors.New("user account already exists") // ErrNoUserSlotsAvailable is returned when there are no user account slots available ErrNoUserSlotsAvailable = errors.New("no user account slots available") // ErrUserAccountNotFound is returned when the user account is not present ErrUserAccountNotFound = errors.New("given user account does not exist") // ErrUserAccountUpdate is returned when the user account failed to be updated ErrUserAccountUpdate = errors.New("user account attributes could not be updated") // ErrRedfishVersionIncompatible is returned when a given version of redfish doesn't support a feature ErrRedfishVersionIncompatible = errors.New("operation not supported in this redfish version") // ErrRedfishChassisOdataID is returned when no compatible Chassis Odata IDs were identified ErrRedfishChassisOdataID = errors.New("no compatible Chassis Odata IDs identified") // ErrRedfishSystemOdataID is returned when no compatible System Odata IDs were identified ErrRedfishSystemOdataID = errors.New("no compatible System Odata IDs identified") // ErrRedfishManagerOdataID is returned when no compatible Manager Odata IDs were identified ErrRedfishManagerOdataID = errors.New("no compatible Manager Odata IDs identified") // ErrRedfishServiceNil is returned when a redfish method is invoked on a nil redfish (gofish) Service object ErrRedfishServiceNil = errors.New("redfish connection returned a nil redfish Service object") // ErrRedfishSoftwareInventory is returned when software inventory could not be collected over redfish ErrRedfishSoftwareInventory = errors.New("error collecting redfish software inventory") // ErrFirmwareUpload is returned when a firmware upload method fails ErrFirmwareUpload = errors.New("error uploading firmware") // ErrFirmwareInstall is returned for firmware install failures ErrFirmwareInstall = errors.New("error updating firmware") // ErrFirmwareInstallUploaded is returned for a firmware install call on a firmware previously uploaded. ErrFirmwareInstallUploaded = errors.New("error installing uploaded firmware") // ErrFirmwareInstallStatus is returned for firmware install status read ErrFirmwareInstallStatus = errors.New("error querying firmware install status") // ErrFirmwareTaskStatus is returned when a query for the firmware upload status fails ErrFirmwareTaskStatus = errors.New("error querying firmware upload status") // ErrFirmwareVerifyTask indicates a firmware verify task is in progress or did not complete successfully, ErrFirmwareVerifyTask = errors.New("error firmware upload verify task") // ErrRedfishUpdateService is returned on redfish update service errors ErrRedfishUpdateService = errors.New("redfish update service error") // ErrTaskNotFound is returned when the (redfish) task could not be found ErrTaskNotFound = errors.New("task not found") // ErrTaskPurge is returned when a (redfish) task could not be purged ErrTaskPurge = errors.New("unable to purge task") // ErrPowerStatusRead is returned when a power status read query fails ErrPowerStatusRead = errors.New("error returning power status") // ErrPowerStatusSet is returned when a power status set query fails ErrPowerStatusSet = errors.New("error setting power status") // ErrProviderImplementation is returned when theres an error in the BMC provider implementation ErrProviderImplementation = errors.New("error in provider implementation") // ErrCompatibilityCheck is returned when the compatibility probe failed to complete successfully. ErrCompatibilityCheck = errors.New("compatibility check failed") // ErrNoBiosAttributes is returned when no bios attributes are available from the BMC. ErrNoBiosAttributes = errors.New("no BIOS attributes available") // ErrScreenshot is returned when screen capture fails. ErrScreenshot = errors.New("error in capturing screen") // ErrIncompatibleProvider is returned by Open() when the device is not compatible with the provider ErrIncompatibleProvider = errors.New("provider not compatible with device") // ErrBMCColdResetRequired is returned when a BMC cold reset is required. ErrBMCColdResetRequired = errors.New("BMC cold reset required") // ErrHostPowercycleRequired is returned when a host powercycle is required. ErrHostPowercycleRequired = errors.New("Host power cycle required") // ErrSessionExpired is returned when the BMC session is not valid // the receiver can then choose to request a new session. ErrSessionExpired = errors.New("session expired") // ErrSystemVendorModel is returned when the system vendor, model attributes could not be identified. ErrSystemVendorModel = errors.New("error identifying system vendor, model attributes") // ErrRedfishNoSystems is returned when the API of the device provides and empty array of systems. ErrRedfishNoSystems = errors.New("redfish: no Systems were found on the device") // ErrBMCUpdating is returned when the BMC is going through an update and will not serve other queries. ErrBMCUpdating = errors.New("a BMC firmware update is in progress") ) type ErrUnsupportedHardware struct { msg string } func (e *ErrUnsupportedHardware) Error() string { return fmt.Sprintf("Hardware not supported: %s", e.msg) } func NewErrUnsupportedHardware(s string) error { return &ErrUnsupportedHardware{s} } ================================================ FILE: examples/bios/doc.go ================================================ /* bios is an example commmand that retrieves BIOS configuration information and prints it out $ BMC_HOST=1.2.3.4 BMC_USERNAME=foo BMC_PASSWORD=bar go run ./examples/bios/main.go */ package main ================================================ FILE: examples/bios/main.go ================================================ package main import ( "context" "encoding/json" "flag" "fmt" "io" "os" "strings" "time" bmclib "github.com/bmc-toolbox/bmclib/v2" "github.com/bmc-toolbox/bmclib/v2/providers" logrusr "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() // Command line option flag parsing user := flag.String("user", "", "Username to login with") pass := flag.String("password", "", "Username to login with") host := flag.String("host", "", "BMC hostname to connect to") mode := flag.String("mode", "get", "Mode [get,set,reset]") dfile := flag.String("file", "", "Read data from file") flag.Parse() // Logger configuration l := logrus.New() l.Level = logrus.DebugLevel // l.Level = logrus.TraceLevel logger := logrusr.New(l) logger.V(9) // bmclib client abstraction clientOpts := []bmclib.Option{bmclib.WithLogger(logger)} client := bmclib.NewClient(*host, *user, *pass, clientOpts...) client.Registry.Drivers = client.Registry.Supports( providers.FeatureGetBiosConfiguration, providers.FeatureSetBiosConfiguration, providers.FeatureResetBiosConfiguration, providers.FeatureSetBiosConfigurationFromFile) err := client.Open(ctx) if err != nil { l.Fatal(err, "bmc login failed") } defer client.Close(ctx) // Operating mode selection switch strings.ToLower(*mode) { case "get": // retrieve bios configuration biosConfig, err := client.GetBiosConfiguration(ctx) if err != nil { l.Fatal(err) } fmt.Printf("biosConfig: %#v\n", biosConfig) case "set": exampleConfig := make(map[string]string) if *dfile != "" { jsonFile, err := os.Open(*dfile) if err != nil { l.Fatal(err) } defer jsonFile.Close() jsonData, _ := io.ReadAll(jsonFile) err = json.Unmarshal(jsonData, &exampleConfig) if err != nil { l.Fatal(err) } } else { exampleConfig["TpmSecurity"] = "Off" } fmt.Println("Attempting to set BIOS configuration:") fmt.Printf("exampleConfig: %+v\n", exampleConfig) err := client.SetBiosConfiguration(ctx, exampleConfig) if err != nil { l.Error(err) } case "setfile": fmt.Println("Attempting to set BIOS configuration:") contents, err := os.ReadFile(*dfile) if err != nil { l.Fatal(err) } err = client.SetBiosConfigurationFromFile(ctx, string(contents)) if err != nil { l.Error(err) } case "reset": err := client.ResetBiosConfiguration(ctx) if err != nil { l.Error(err) } default: l.Fatal("Unknown mode: " + *mode) } } ================================================ FILE: examples/create-users/doc.go ================================================ /* create-users is an example commmand that utilizes the 'v1' bmclib interface methods to create user entries in a BMC using the redfish driver. $ go run ./examples/v1/create-users/main.go -h Usage of /tmp/go-build440589615/b001/exe/main: -cert-pool string Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true -dry-run Connect to the BMC but do not create users -host string BMC hostname to connect to -password string Username to login with -port int BMC port to connect to (default 443) -secure-tls Enable secure TLS -user string Username to login with -user-csv string A CSV file of users to create containing 3 columns: username, password, role */ package main ================================================ FILE: examples/create-users/main.go ================================================ package main import ( "context" "crypto/x509" "encoding/csv" "flag" "io" "io/ioutil" "os" "time" bmclib "github.com/bmc-toolbox/bmclib/v2" "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) func main() { user := flag.String("user", "", "Username to login with") pass := flag.String("password", "", "Username to login with") host := flag.String("host", "", "BMC hostname to connect to") withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") certPoolFile := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") userCSV := flag.String("user-csv", "", "A CSV file of users to create containing 3 columns: username, password, role") dryRun := flag.Bool("dry-run", false, "Connect to the BMC but do not create users") flag.Parse() l := logrus.New() l.Level = logrus.DebugLevel logger := logrusr.New(l) if *host == "" || *user == "" || *pass == "" { l.Fatal("required host/user/pass parameters not defined") } clientOpts := []bmclib.Option{bmclib.WithLogger(logger)} if *withSecureTLS { var pool *x509.CertPool if *certPoolFile != "" { pool = x509.NewCertPool() data, err := ioutil.ReadFile(*certPoolFile) if err != nil { l.Fatal(err) } pool.AppendCertsFromPEM(data) } // a nil pool uses the system certs clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) } cl := bmclib.NewClient(*host, *user, *pass, clientOpts...) cl.Registry.Drivers = cl.Registry.Using("redfish") ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() err := cl.Open(ctx) if err != nil { l.WithError(err).Fatal(err, "BMC login failed") } defer cl.Close(ctx) fh, err := os.Open(*userCSV) if err != nil { l.WithError(err).WithField("file", *userCSV).Fatal() } defer fh.Close() reader := csv.NewReader(fh) i := 0 for { record, err := reader.Read() i++ if err == io.EOF { break } if err != nil { l.WithError(err).Fatal() } if len(record) != 3 { l.WithField("line", i).WithField("length", len(record)).Infof("line did not have 3 columns") continue } if !*dryRun { _, err = cl.CreateUser(ctx, record[0], record[1], record[2]) if err != nil { l.WithError(err).Error("error creating user") continue } } l.WithFields(logrus.Fields(map[string]interface{}{ "user": record[0], "role": record[2], })).Info("created user") } l.WithField("count", i).Info("created users") } ================================================ FILE: examples/floppy-image/doc.go ================================================ /* inventory is an example commmand that utilizes the 'v1' bmclib interface methods to upload and mount, unmount a floppy image. # mount image $ go run examples/floppy-image/main.go \ -host 10.1.2.3 \ -user ADMIN \ -password hunter2 \ -image /tmp/floppy.img # un-mount image $ go run examples/floppy-image/main.go \ -host 10.1.2.3 \ -user ADMIN \ -password hunter2 \ -unmount */ package main ================================================ FILE: examples/floppy-image/main.go ================================================ package main import ( "context" "crypto/x509" "flag" "log" "os" "time" bmclib "github.com/bmc-toolbox/bmclib/v2" "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() user := flag.String("user", "", "Username to login with") pass := flag.String("password", "", "Username to login with") host := flag.String("host", "", "BMC hostname to connect to") imagePath := flag.String("image", "", "The .img file to be uploaded") unmountImage := flag.Bool("unmount", false, "Unmount floppy image") withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") certPoolFile := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") flag.Parse() l := logrus.New() l.Level = logrus.DebugLevel logger := logrusr.New(l) clientOpts := []bmclib.Option{bmclib.WithLogger(logger)} if *withSecureTLS { var pool *x509.CertPool if *certPoolFile != "" { pool = x509.NewCertPool() data, err := os.ReadFile(*certPoolFile) if err != nil { l.Fatal(err) } pool.AppendCertsFromPEM(data) } // a nil pool uses the system certs clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) } cl := bmclib.NewClient(*host, *user, *pass, clientOpts...) err := cl.Open(ctx) if err != nil { log.Fatal(err, "bmc login failed") } defer cl.Close(ctx) if *unmountImage { if err := cl.UnmountFloppyImage(ctx); err != nil { log.Fatal(err) } return } // open file handle fh, err := os.Open(*imagePath) if err != nil { l.Fatal(err) } defer fh.Close() err = cl.MountFloppyImage(ctx, fh) if err != nil { l.Fatal(err) } l.WithField("img", *imagePath).Info("image mounted successfully") } ================================================ FILE: examples/homeassistant/main.go ================================================ package main import ( "context" "time" "github.com/bmc-toolbox/bmclib/v2" "github.com/bmc-toolbox/bmclib/v2/logging" "github.com/bmc-toolbox/bmclib/v2/providers/homeassistant" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Start the test consumer time.Sleep(100 * time.Millisecond) log := logging.ZeroLogger("info") opts := []bmclib.Option{ bmclib.WithLogger(log), //bmclib.WithPerProviderTimeout(5 * time.Second), bmclib.WithHomeAssistantOpt(homeassistant.Config{ SwitchEntityID: "switch.shellypstripg4_98a3167b747c_switch_0", PowerOperationDelaySeconds: 2, }), } host := "http://some.homeassistant.instance:8123" user := "notuseduser" pass := "ey.....hk" c := bmclib.NewClient(host, user, pass, opts...) if err := c.Open(ctx); err != nil { panic(err) } defer c.Close(ctx) ok3, err := c.SetBootDevice(ctx, "pxe", false, false) if err != nil { panic(err) } log.Info("set boot device", "ok3", ok3) state, err := c.GetPowerState(ctx) if err != nil { panic(err) } log.Info("power state", "state", state) log.Info("metadata for GetPowerState", "metadata", c.GetMetadata()) ok, err := c.SetPowerState(ctx, "on") if err != nil { panic(err) } log.Info("set power state ON", "ok", ok) log.Info("metadata for SetPowerState ON", "metadata", c.GetMetadata()) ok2, err := c.SetPowerState(ctx, "off") if err != nil { panic(err) } log.Info("set power state OFF", "ok2", ok2) log.Info("metadata for SetPowerState OFF", "metadata", c.GetMetadata()) <-ctx.Done() } ================================================ FILE: examples/install-firmware/doc.go ================================================ /* install-firmware is an example command that utilizes the 'v1' bmclib interface methods to flash a firmware image to a BMC. Note: The example installs the firmware and polls until the status until the install is complete, and if required by the install process - power cycles the host. $ go run ./examples/v1/install-firmware/main.go -h Usage of /tmp/go-build2950657412/b001/exe/main: -cert-pool string Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when -secure-tls=true -firmware string The local path of the firmware to install -host string BMC hostname to connect to -password string Username to login with -port int BMC port to connect to (default 443) -secure-tls Enable secure TLS -user string Username to login with -version string The firmware version being installed # install bios firmware on a supermicro X11 # $ go run . -host 192.168.1.1 -user ADMIN -password hunter2 -component bios -firmware BIOS_X11DPH-0981_20220208_3.6_STD.bin INFO[0007] set firmware install mode component=BIOS ip="https://192.168.1.1" model=X11DPH-T INFO[0011] uploading firmware component=BIOS ip="https://192.168.1.1" model=X11DPH-T INFO[0091] verifying uploaded firmware component=BIOS ip="https://192.168.1.1" model=X11DPH-T INFO[0105] initiating firmware install component=BIOS ip="https://192.168.1.1" model=X11DPH-T INFO[0115] firmware install running component=bios state=running INFO[0132] firmware install running component=bios state=running ... ... INFO[0628] firmware install running component=bios state=running INFO[0635] host powercycle required component=bios state=powercycle-host INFO[0637] host power cycled, all done! component=bios state=powercycle-host # install bmc firmware on a supermicro X11 # $ go run . -host 192.168.1.1 -user ADMIN -password hunter2 -component bmc -firmware BMC_X11AST2500-4101MS_20220225_01.74.02_STD.bin INFO[0007] setting device to firmware install mode component=BMC ip="https://192.168.1.1" INFO[0009] uploading firmware ip="https://192.168.1.1" INFO[0045] verifying uploaded firmware ip="https://192.168.1.1" INFO[0047] initiating firmware install ip="https://192.168.1.1" INFO[0079] firmware install running component=bmc state=running INFO[0085] firmware install running component=bmc state=running ... ... INFO[0233] firmware install running component=bmc state=running INFO[0238] firmware install completed component=bmc state=complete */ package main ================================================ FILE: examples/install-firmware/main.go ================================================ package main import ( "context" "crypto/x509" "errors" "flag" "io/ioutil" "log" "os" "strings" "time" bmclib "github.com/bmc-toolbox/bmclib/v2" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) func main() { user := flag.String("user", "", "Username to login with") pass := flag.String("password", "", "Username to login with") host := flag.String("host", "", "BMC hostname to connect to") component := flag.String("component", "", "Component to be updated (bmc, bios.. etc)") withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") certPoolPath := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") firmwarePath := flag.String("firmware", "", "The local path of the firmware to install") firmwareVersion := flag.String("version", "", "The firmware version being installed") flag.Parse() ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute) defer cancel() l := logrus.New() l.Level = logrus.TraceLevel logger := logrusr.New(l) if *host == "" || *user == "" || *pass == "" { l.Fatal("required host/user/pass parameters not defined") } if *component == "" { l.Fatal("component parameter required (must be a component slug - bmc, bios etc)") } clientOpts := []bmclib.Option{ bmclib.WithLogger(logger), bmclib.WithPerProviderTimeout(time.Minute * 30), } if *withSecureTLS { var pool *x509.CertPool if *certPoolPath != "" { pool = x509.NewCertPool() data, err := ioutil.ReadFile(*certPoolPath) if err != nil { l.Fatal(err) } pool.AppendCertsFromPEM(data) } // a nil pool uses the system certs clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) } cl := bmclib.NewClient(*host, *user, *pass, clientOpts...) err := cl.Open(ctx) if err != nil { l.Fatal(err, "bmc login failed") } defer cl.Close(ctx) // open file handle fh, err := os.Open(*firmwarePath) if err != nil { l.Fatal(err) } defer fh.Close() taskID, err := cl.FirmwareInstall(ctx, *component, string(constants.OnReset), true, fh) if err != nil { l.Fatal(err) } for { if ctx.Err() != nil { l.Fatal(ctx.Err()) } state, err := cl.FirmwareInstallStatus(ctx, *firmwareVersion, *component, taskID) if err != nil { // when its under update a connection refused is returned if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "operation timed out") { l.Info("BMC refused connection, BMC most likely resetting...") time.Sleep(2 * time.Second) continue } if errors.Is(err, bmclibErrs.ErrSessionExpired) || strings.Contains(err.Error(), "session expired") { err := cl.Open(ctx) if err != nil { l.Fatal(err, "bmc re-login failed") } l.WithFields(logrus.Fields{"state": state, "component": *component}).Info("BMC session expired, logging in...") continue } log.Fatal(err) } switch state { case constants.FirmwareInstallRunning, constants.FirmwareInstallInitializing: l.WithFields(logrus.Fields{"state": state, "component": *component}).Info("firmware install running") case constants.FirmwareInstallFailed: l.WithFields(logrus.Fields{"state": state, "component": *component}).Info("firmware install failed") os.Exit(1) case constants.FirmwareInstallComplete: l.WithFields(logrus.Fields{"state": state, "component": *component}).Info("firmware install completed") os.Exit(0) case constants.FirmwareInstallPowerCycleHost: l.WithFields(logrus.Fields{"state": state, "component": *component}).Info("host powercycle required") if _, err := cl.SetPowerState(ctx, "cycle"); err != nil { l.WithFields(logrus.Fields{"state": state, "component": *component}).Info("error power cycling host for install") os.Exit(1) } l.WithFields(logrus.Fields{"state": state, "component": *component}).Info("host power cycled, all done!") os.Exit(0) default: l.WithFields(logrus.Fields{"state": state, "component": *component}).Info("unknown state returned") } time.Sleep(2 * time.Second) } } ================================================ FILE: examples/inventory/doc.go ================================================ /* inventory is an example commmand that utilizes the 'v1' bmclib interface methods to gather inventory from a BMC using the redfish driver. $ go run ./examples/v1/inventory/main.go -h Usage of /tmp/go-build1853609647/b001/exe/main: -cert-pool string Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true -host string BMC hostname to connect to -password string Username to login with -port int BMC port to connect to (default 443) -secure-tls Enable secure TLS -user string Username to login with */ package main ================================================ FILE: examples/inventory/main.go ================================================ package main import ( "context" "crypto/x509" "encoding/json" "flag" "fmt" "log" "os" "strings" "time" bmclib "github.com/bmc-toolbox/bmclib/v2" "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() user := flag.String("user", "", "Username to login with") pass := flag.String("password", "", "Username to login with") host := flag.String("host", "", "BMC hostname to connect to") incompatibleRedfishVersions := flag.String("incompatible-redfish-versions", "", "Comma separated list of redfish versions to deem incompatible") withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") certPoolFile := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") flag.Parse() l := logrus.New() l.Level = logrus.DebugLevel logger := logrusr.New(l) clientOpts := []bmclib.Option{bmclib.WithLogger(logger)} if *withSecureTLS { var pool *x509.CertPool if *certPoolFile != "" { pool = x509.NewCertPool() data, err := os.ReadFile(*certPoolFile) if err != nil { l.Fatal(err) } pool.AppendCertsFromPEM(data) } // a nil pool uses the system certs clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) } if len(*incompatibleRedfishVersions) > 0 { // blacklist a redfish version clientOpts = append( clientOpts, bmclib.WithRedfishVersionsNotCompatible(strings.Split(*incompatibleRedfishVersions, ","))) } cl := bmclib.NewClient(*host, *user, *pass, clientOpts...) cl.Registry.Drivers = cl.Registry.FilterForCompatible(ctx) err := cl.Open(ctx) if err != nil { log.Fatal(err, "bmc login failed") } defer cl.Close(ctx) inv, err := cl.Inventory(ctx) if err != nil { l.Error(err) } b, err := json.MarshalIndent(inv, "", " ") if err != nil { l.Error(err) } fmt.Println(string(b)) } ================================================ FILE: examples/inventory/output.json ================================================ { "oem": false, "vendor": "Dell Inc.", "model": "PowerEdge R6515", "serial": "FOOBAR", "bios": { "description": "BIOS Configuration Current Settings", "firmware": { "installed": "2.2.4", "software_id": "159", "previous": [ { "installed": "1.3.1", "software_id": "159" } ], "metadata": { "name": "BIOS" } } }, "bmc": { "id": "iDRAC.Embedded.1", "description": "BMC", "vendor": "Dell Inc.", "model": "PowerEdge R6515", "status": { "Health": "OK", "State": "Enabled" }, "firmware": { "installed": "5.00.00.00", "software_id": "25227", "previous": [ { "installed": "4.10.10.10", "software_id": "25227" } ], "metadata": { "name": "Integrated Dell Remote Access Controller" } } }, "mainboard": {}, "cplds": [ { "description": "System CPLD", "vendor": "Dell Inc.", "model": "PowerEdge R6515", "firmware": { "installed": "1.0.3", "software_id": "27763", "metadata": { "name": "System CPLD" } } } ], "tpms": [ { "interface_type": "TPM2_0", "firmware": { "installed": "1.3.1.0", "software_id": "109673", "metadata": { "name": "TPM" } }, "status": { "Health": "", "State": "Enabled" } } ], "cpus": [ { "id": "CPU.Socket.1", "description": "Represents the properties of a Processor attached to this System", "vendor": "AMD", "model": "AMD EPYC 7402P 24-Core Processor", "slot": "CPU.Socket.1", "architecture": "x86", "clock_speeed_hz": 3900, "cores": 24, "threads": 48, "status": { "Health": "OK", "State": "Enabled" }, "firmware": { "installed": "0x830104D" } } ], "memory": [ { "description": "DIMM A7", "slot": "DIMM.Socket.A7", "type": "DRAM", "vendor": "Hynix Semiconductor", "serial": "537385EF", "size_bytes": 8192, "part_number": "HMA81GR7CJR8N-XN", "clock_speed_hz": 3200, "status": { "Health": "OK", "State": "Enabled" } }, { "description": "DIMM A6", "slot": "DIMM.Socket.A6", "type": "DRAM", "vendor": "Hynix Semiconductor", "serial": "53738538", "size_bytes": 8192, "part_number": "HMA81GR7CJR8N-XN", "clock_speed_hz": 3200, "status": { "Health": "OK", "State": "Enabled" } }, { "description": "DIMM A3", "slot": "DIMM.Socket.A3", "type": "DRAM", "vendor": "Hynix Semiconductor", "serial": "53738536", "size_bytes": 8192, "part_number": "HMA81GR7CJR8N-XN", "clock_speed_hz": 3200, "status": { "Health": "OK", "State": "Enabled" } }, { "description": "DIMM A8", "slot": "DIMM.Socket.A8", "type": "DRAM", "vendor": "Hynix Semiconductor", "serial": "5373856E", "size_bytes": 8192, "part_number": "HMA81GR7CJR8N-XN", "clock_speed_hz": 3200, "status": { "Health": "OK", "State": "Enabled" } }, { "description": "DIMM A2", "slot": "DIMM.Socket.A2", "type": "DRAM", "vendor": "Hynix Semiconductor", "serial": "5373858E", "size_bytes": 8192, "part_number": "HMA81GR7CJR8N-XN", "clock_speed_hz": 3200, "status": { "Health": "OK", "State": "Enabled" } }, { "description": "DIMM A4", "slot": "DIMM.Socket.A4", "type": "DRAM", "vendor": "Hynix Semiconductor", "serial": "537385CA", "size_bytes": 8192, "part_number": "HMA81GR7CJR8N-XN", "clock_speed_hz": 3200, "status": { "Health": "OK", "State": "Enabled" } }, { "description": "DIMM A5", "slot": "DIMM.Socket.A5", "type": "DRAM", "vendor": "Hynix Semiconductor", "serial": "53738537", "size_bytes": 8192, "part_number": "HMA81GR7CJR8N-XN", "clock_speed_hz": 3200, "status": { "Health": "OK", "State": "Enabled" } }, { "description": "DIMM A1", "slot": "DIMM.Socket.A1", "type": "DRAM", "vendor": "Hynix Semiconductor", "serial": "53738568", "size_bytes": 8192, "part_number": "HMA81GR7CJR8N-XN", "clock_speed_hz": 3200, "status": { "Health": "OK", "State": "Enabled" } } ], "nics": [ { "id": "NIC.Embedded.1", "description": "Embedded NIC 1 Port 1 Partition 1", "speed_bits": 6, "oem": false, "metadata": null, "status": { "Health": "OK", "State": "Enabled" }, "firmware": { "installed": "1.5.12" } }, { "id": "NIC.Slot.3", "description": "NIC in Slot 3 Port 2 Partition 1", "vendor": "Intel Corporation", "model": "Intel(R) 10GbE 2P X710 Adapter", "serial": "MYFLMIT06404GD", "speed_bits": 100006, "macaddress": "F8:F2:1E:A6:89:A1", "oem": false, "metadata": null, "status": { "Health": "OK", "State": "Enabled" }, "firmware": { "installed": "20.0.17", "software_id": "102302", "previous": [ { "installed": "19.5.12", "software_id": "102301" }, { "installed": "19.5.12", "software_id": "102302" } ], "metadata": { "name": "Intel(R) Ethernet Converged Network Adapter X710 - F8:F2:1E:A6:89:A1" } } } ], "drives": [ { "id": "Disk.Bay.1:Enclosure.Internal.0-1:NonRAID.Integrated.1-1", "name": "MZ7LH480HBHQ0D3", "drive_type": "SSD", "description": "Disk 1 in Backplane 1 of Integrated Storage Controller 1", "serial": "S5YJNE0N503923", "storage_controller": "NonRAID.Integrated.1-1", "vendor": "SAMSUNG", "model": "MZ7LH480HBHQ0D3", "protocol": "SATA", "capacity_bytes": 480103980544, "block_size_bytes": 512, "capable_speed_gbps": 6, "negotiated_speed_gbps": 6, "firmware": { "installed": "HG58", "software_id": "108622", "metadata": { "name": "Disk 1 in Backplane 1 of Integrated Storage Controller 1" } }, "status": { "Health": "OK", "State": "Enabled" } }, { "id": "Disk.Bay.0:Enclosure.Internal.0-1:NonRAID.Integrated.1-1", "name": "MZ7LH480HBHQ0D3", "drive_type": "SSD", "description": "Disk 0 in Backplane 1 of Integrated Storage Controller 1", "serial": "S5YJNE0N503924", "storage_controller": "NonRAID.Integrated.1-1", "vendor": "SAMSUNG", "model": "MZ7LH480HBHQ0D3", "protocol": "SATA", "capacity_bytes": 480103980544, "block_size_bytes": 512, "capable_speed_gbps": 6, "negotiated_speed_gbps": 6, "firmware": { "installed": "HG58", "software_id": "108622", "metadata": { "name": "Disk 0 in Backplane 1 of Integrated Storage Controller 1" } }, "status": { "Health": "OK", "State": "Enabled" } }, { "id": "Disk.Direct.0-0:AHCI.Slot.2-1", "name": "SSDSCKKB240G8R", "drive_type": "SSD", "description": "Disk 0 on AHCI Controller in slot 2", "serial": "PHYH011400V5240J", "storage_controller": "AHCI.Slot.2-1", "vendor": "INTEL", "model": "SSDSCKKB240G8R", "protocol": "SATA", "capacity_bytes": 240057409536, "block_size_bytes": 512, "capable_speed_gbps": 6, "negotiated_speed_gbps": 6, "firmware": { "installed": "DL6R", "software_id": "108313", "metadata": { "name": "Disk 0 on AHCI Controller in slot 2" } }, "status": { "Health": "OK", "State": "Enabled" } }, { "id": "Disk.Direct.1-1:AHCI.Slot.2-1", "name": "SSDSCKKB240G8R", "drive_type": "SSD", "description": "Disk 1 on AHCI Controller in slot 2", "serial": "PHYH011303RM240J", "storage_controller": "AHCI.Slot.2-1", "vendor": "INTEL", "model": "SSDSCKKB240G8R", "protocol": "SATA", "capacity_bytes": 240057409536, "block_size_bytes": 512, "capable_speed_gbps": 6, "negotiated_speed_gbps": 6, "firmware": { "installed": "DL6R", "software_id": "108313", "metadata": { "name": "Disk 1 on AHCI Controller in slot 2" } }, "status": { "Health": "OK", "State": "Enabled" } } ], "storage_controller": [ { "description": "Dell HBA330 Mini", "vendor": "DELL", "speed_gbps": 12, "oem": false, "status": { "Health": "OK", "State": "Enabled" }, "metadata": null, "firmware": { "installed": "16.17.01.00", "software_id": "104298", "metadata": { "name": "Dell HBA330 Mini" } } }, { "description": "BOSS-S1", "vendor": "DELL", "speed_gbps": 6, "oem": false, "status": { "Health": "OK", "State": "Enabled" }, "metadata": null, "firmware": { "installed": "2.5.13.3024", "software_id": "106883", "metadata": { "name": "BOSS-S1" } } } ], "power_supplies": [ { "description": "PS1 Status", "vendor": "DELL", "model": "PWR SPLY,550W,RDNT,DELTA", "serial": "CNDED000330FBL", "power_capacity_watts": 550, "oem": false, "status": { "Health": "OK", "State": "Enabled" }, "firmware": { "installed": "00.0C.7D" } }, { "description": "PS2 Status", "vendor": "DELL", "model": "PWR SPLY,550W,RDNT,DELTA", "serial": "CNDED000330FFI", "power_capacity_watts": 550, "oem": false, "status": { "Health": "OK", "State": "Enabled" }, "firmware": { "installed": "00.0C.7D" } } ], "enclosures": [ { "id": "System.Embedded.1", "description": "It represents the properties for physical components for any system.It represent racks, rackmount servers, blades, standalone, modular systems,enclosures, and all other containers.The non-cpu/device centric parts of the schema are all accessed either directly or indirectly through this resource.", "chassis_type": "RackMount", "vendor": "Dell Inc.", "model": "PowerEdge R6515", "firmware": {}, "status": { "Health": "OK", "State": "Enabled" } }, { "id": "Enclosure.Internal.0-1:NonRAID.Integrated.1-1", "description": "Backplane 1 on Connector 0 of Integrated Storage Controller 1", "chassis_type": "StorageEnclosure", "model": "BP14G+ 0:1", "firmware": { "installed": "HG58", "software_id": "108622", "metadata": { "name": "Disk 1 in Backplane 1 of Integrated Storage Controller 1" } }, "status": { "Health": "OK", "State": "Enabled" } }, { "id": "Enclosure.Internal.0-1", "description": "PCIe SSD Backplane 1", "chassis_type": "StorageEnclosure", "model": "PCIe SSD Backplane 1", "firmware": { "installed": "HG58", "software_id": "108622", "metadata": { "name": "Disk 1 in Backplane 1 of Integrated Storage Controller 1" } }, "status": { "Health": "OK", "State": "Enabled" } } ], "status": { "Health": "", "State": "" } } ================================================ FILE: examples/reset_bmc/reset_bmc.go ================================================ package main import ( "context" "log" "os" "time" "github.com/bmc-toolbox/bmclib/v2" "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() // set BMC parameters here host := "10.211.132.157" user := "root" pass := "yxvZdxAQ38ZWlZ" l := logrus.New() l.Level = logrus.DebugLevel logger := logrusr.New(l) if host == "" || user == "" || pass == "" { log.Fatal("required host/user/pass parameters not defined") } os.Setenv("DEBUG_BMCLIB", "true") defer os.Unsetenv("DEBUG_BMCLIB") cl := bmclib.NewClient(host, user, pass, bmclib.WithLogger(logger)) err := cl.Open(ctx) if err != nil { log.Fatal(err, "bmc login failed") } defer cl.Close(ctx) _, err = cl.ResetBMC(ctx, "GracefulRestart") if err != nil { l.Error(err) } } ================================================ FILE: examples/rpc/main.go ================================================ package main import ( "context" "encoding/json" "net/http" "time" "github.com/bmc-toolbox/bmclib/v2" "github.com/bmc-toolbox/bmclib/v2/logging" "github.com/bmc-toolbox/bmclib/v2/providers/rpc" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Start the test consumer go testConsumer(ctx) time.Sleep(100 * time.Millisecond) log := logging.ZeroLogger("info") opts := []bmclib.Option{ bmclib.WithLogger(log), bmclib.WithPerProviderTimeout(5 * time.Second), bmclib.WithRPCOpt(rpc.Provider{ ConsumerURL: "http://localhost:8800", // Opts are not required. Opts: rpc.Opts{ HMAC: rpc.HMACOpts{ Secrets: rpc.Secrets{rpc.SHA256: {"superSecret1"}}, }, Signature: rpc.SignatureOpts{ HeaderName: "X-Bespoke-Signature", IncludedPayloadHeaders: []string{"X-Bespoke-Timestamp"}, }, Request: rpc.RequestOpts{ TimestampHeader: "X-Bespoke-Timestamp", }, }, }), } host := "127.0.1.1" user := "admin" pass := "admin" c := bmclib.NewClient(host, user, pass, opts...) if err := c.Open(ctx); err != nil { panic(err) } defer c.Close(ctx) state, err := c.GetPowerState(ctx) if err != nil { panic(err) } log.Info("power state", "state", state) log.Info("metadata for GetPowerState", "metadata", c.GetMetadata()) ok, err := c.SetPowerState(ctx, "on") if err != nil { panic(err) } log.Info("set power state", "ok", ok) log.Info("metadata for SetPowerState", "metadata", c.GetMetadata()) <-ctx.Done() } func testConsumer(ctx context.Context) error { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { req := rpc.RequestPayload{} if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) return } rp := rpc.ResponsePayload{ ID: req.ID, Host: req.Host, } switch req.Method { case rpc.PowerGetMethod: rp.Result = "on" case rpc.PowerSetMethod: case rpc.BootDeviceMethod: case rpc.PingMethod: rp.Result = "pong" default: w.WriteHeader(http.StatusNotFound) } b, _ := json.Marshal(rp) w.Write(b) }) return http.ListenAndServe(":8800", nil) } ================================================ FILE: examples/screenshot/doc.go ================================================ /* status is an example commmand that utilizes the 'v1' bmclib interface methods to capture a screenshot. $ go run ./examples/v1/status/main.go -h Usage of /tmp/go-build1941100323/b001/exe/main: -cert-pool string Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true -host string BMC hostname to connect to -password string Username to login with -port int BMC port to connect to (default 443) -secure-tls Enable secure TLS -user string Username to login with */ package main ================================================ FILE: examples/screenshot/main.go ================================================ package main import ( "context" "crypto/x509" "flag" "fmt" "io/ioutil" "os" "time" "github.com/bmc-toolbox/bmclib/v2" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) func main() { user := flag.String("user", "", "Username to login with") pass := flag.String("password", "", "Username to login with") host := flag.String("host", "", "BMC hostname to connect to") port := flag.String("port", "443", "BMC port to connect to") withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") certPoolFile := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") flag.Parse() l := logrus.New() l.Level = logrus.DebugLevel logger := logrusr.New(l) if *host == "" || *user == "" || *pass == "" { l.Fatal("required host/user/pass parameters not defined") } clientOpts := []bmclib.Option{ bmclib.WithLogger(logger), bmclib.WithRedfishPort(*port), } if *withSecureTLS { var pool *x509.CertPool if *certPoolFile != "" { pool = x509.NewCertPool() data, err := ioutil.ReadFile(*certPoolFile) if err != nil { l.Fatal(err) } pool.AppendCertsFromPEM(data) } // a nil pool uses the system certs clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) } cl := bmclib.NewClient(*host, *user, *pass, clientOpts...) cl.Registry.Drivers = cl.Registry.Supports(providers.FeatureScreenshot) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() err := cl.Open(ctx) if err != nil { l.WithError(err).Fatal(err, "BMC login failed") return } defer cl.Close(ctx) image, fileType, err := cl.Screenshot(ctx) if err != nil { l.WithError(err).Error() return } filename := fmt.Sprintf("screenshot." + fileType) fh, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0600) if err != nil { l.WithError(err).Error() return } defer fh.Close() _, err = fh.Write(image) if err != nil { l.WithError(err).Error() return } l.Info("screenshot saved as: " + filename) } ================================================ FILE: examples/sel/main.go ================================================ package main import ( "context" "crypto/x509" "flag" "io/ioutil" "time" "github.com/bmc-toolbox/bmclib/v2" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) func main() { user := flag.String("user", "", "Username to login with") pass := flag.String("password", "", "Username to login with") host := flag.String("host", "", "BMC hostname to connect to") withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") certPoolFile := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") action := flag.String("action", "get", "Action to perform on the System Event Log (clear|get)") flag.Parse() l := logrus.New() l.Level = logrus.DebugLevel logger := logrusr.New(l) if *host == "" || *user == "" || *pass == "" { l.Fatal("required host/user/pass parameters not defined") } clientOpts := []bmclib.Option{ bmclib.WithLogger(logger), bmclib.WithRedfishUseBasicAuth(true), } if *withSecureTLS { var pool *x509.CertPool if *certPoolFile != "" { pool = x509.NewCertPool() data, err := ioutil.ReadFile(*certPoolFile) if err != nil { l.Fatal(err) } pool.AppendCertsFromPEM(data) } // a nil pool uses the system certs clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) } cl := bmclib.NewClient(*host, *user, *pass, clientOpts...) cl.Registry.Drivers = cl.Registry.Supports(providers.FeatureClearSystemEventLog) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() err := cl.Open(ctx) if err != nil { l.WithError(err).Fatal(err, "BMC login failed") } defer cl.Close(ctx) switch *action { case "get": entries, err := cl.GetSystemEventLog(ctx) if err != nil { l.WithError(err).Fatal(err, "failed to get System Event Log") } l.Info("System Event Log entries", "entries", entries) return case "get-raw": eventlog, err := cl.GetSystemEventLogRaw(ctx) if err != nil { l.WithError(err).Fatal(err, "failed to get System Event Log Raw") } l.Info("System Event Log", "eventlog", eventlog) return case "clear": err = cl.ClearSystemEventLog(ctx) if err != nil { l.WithError(err).Fatal(err, "failed to clear System Event Log") } l.Info("System Event Log cleared") return default: l.Fatal("invalid action") } } ================================================ FILE: examples/status/doc.go ================================================ /* status is an example commmand that utilizes the 'v1' bmclib interface methods to gather the BMC version, power state, and bios version from a BMC using the redfish driver. $ go run ./examples/v1/status/main.go -h Usage of /tmp/go-build1941100323/b001/exe/main: -cert-pool string Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true -host string BMC hostname to connect to -password string Username to login with -port int BMC port to connect to (default 443) -secure-tls Enable secure TLS -user string Username to login with */ package main ================================================ FILE: examples/status/main.go ================================================ package main import ( "context" "crypto/x509" "flag" "io/ioutil" "time" "github.com/bmc-toolbox/bmclib/v2" "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) func main() { user := flag.String("user", "", "Username to login with") pass := flag.String("password", "", "Username to login with") host := flag.String("host", "", "BMC hostname to connect to") withSecureTLS := flag.Bool("secure-tls", false, "Enable secure TLS") certPoolFile := flag.String("cert-pool", "", "Path to an file containing x509 CAs. An empty string uses the system CAs. Only takes effect when --secure-tls=true") flag.Parse() l := logrus.New() l.Level = logrus.DebugLevel logger := logrusr.New(l) if *host == "" || *user == "" || *pass == "" { l.Fatal("required host/user/pass parameters not defined") } clientOpts := []bmclib.Option{ bmclib.WithLogger(logger), bmclib.WithRedfishUseBasicAuth(true), } if *withSecureTLS { var pool *x509.CertPool if *certPoolFile != "" { pool = x509.NewCertPool() data, err := ioutil.ReadFile(*certPoolFile) if err != nil { l.Fatal(err) } pool.AppendCertsFromPEM(data) } // a nil pool uses the system certs clientOpts = append(clientOpts, bmclib.WithSecureTLS(pool)) } cl := bmclib.NewClient(*host, *user, *pass, clientOpts...) cl.Registry.Drivers = cl.Registry.Using("redfish") ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() err := cl.Open(ctx) if err != nil { l.WithError(err).Fatal(err, "BMC login failed") } defer cl.Close(ctx) state, err := cl.GetPowerState(ctx) if err != nil { l.WithError(err).Error() } l.WithField("power-state", state).Info() } ================================================ FILE: examples/virtualmedia/doc.go ================================================ /* Virtual Media is an example command to mount and umount virtual media (ISO) on a BMC. # mount an ISO $ go run examples/virtualmedia/main.go \ -host 10.1.2.3 \ -user root \ -password calvin \ -iso http://example.com/image.iso # unmount an ISO $ go run examples/virtualmedia/main.go \ -host 10.1.2.3 \ -user root \ -password calvin \ -iso "" */ package main ================================================ FILE: examples/virtualmedia/main.go ================================================ package main import ( "context" "flag" "fmt" "log/slog" "os" "time" "github.com/bmc-toolbox/bmclib/v2" "github.com/go-logr/logr" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() user := flag.String("user", "", "BMC username, required") pass := flag.String("password", "", "BMC password, required") host := flag.String("host", "", "BMC hostname or IP address, required") isoURL := flag.String("iso", "", "The HTTP URL to the ISO to be mounted, leave empty to unmount") flag.Parse() if *user == "" || *pass == "" || *host == "" { fmt.Fprintln(os.Stderr, "user, password, and host are required") flag.PrintDefaults() os.Exit(1) } l := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: true})) log := logr.FromSlogHandler(l.Handler()) cl := bmclib.NewClient(*host, *user, *pass, bmclib.WithLogger(log)) if err := cl.Open(ctx); err != nil { panic(err) } defer cl.Close(ctx) ok, err := cl.SetVirtualMedia(ctx, "CD", *isoURL) if err != nil { log.Info("debugging", "metadata", cl.GetMetadata()) panic(err) } if !ok { log.Info("debugging", "metadata", cl.GetMetadata()) panic("failed virtual media operation") } log.Info("virtual media operation successful", "metadata", cl.GetMetadata()) } ================================================ FILE: filter.go ================================================ package bmclib import "github.com/jacobweinstock/registrar" // PreferProvider reorders the registry to have the given provider first. // This is a one time/temporary reordering of the providers in the registry. // This reorder is not preserved. It is only used for the call that uses the returned Client. // Update the Client.Registry to make the change permanent. For example, `cl.Registry.Drivers = cl.Registry.PreferDriver("ipmitool")` func (c *Client) PreferProvider(name string) *Client { c.oneTimeRegistry.Drivers = c.Registry.PreferDriver(name) c.oneTimeRegistryEnabled = true return c } // Supports removes any provider from the registry that does not support the given features. // This is a one time/temporary reordering of the providers in the registry. // This reorder is not preserved. It is only used for the call that uses the returned Client. func (c *Client) Supports(features ...registrar.Feature) *Client { c.oneTimeRegistry.Drivers = c.Registry.Supports(features...) c.oneTimeRegistryEnabled = true return c } // Using removes any provider from the registry that does not support the given protocol. // This is a one time/temporary reordering of the providers in the registry. // This reorder is not preserved. It is only used for the call that uses the returned Client. func (c *Client) Using(protocol string) *Client { c.oneTimeRegistry.Drivers = c.Registry.Using(protocol) c.oneTimeRegistryEnabled = true return c } // For removes any provider from the registry that is not the given provider. // This is a one time/temporary reordering of the providers in the registry. // This reorder is not preserved. It is only used for the call that uses the returned Client. func (c *Client) For(provider string) *Client { c.oneTimeRegistry.Drivers = c.Registry.For(provider) c.oneTimeRegistryEnabled = true return c } // PreferProtocol reorders the providers in the registry to have the given protocol first. Matching providers order is preserved. // This is a one time/temporary reordering of the providers in the registry. // This reorder is not preserved. It is only used for the call that uses the returned Client. func (c *Client) PreferProtocol(protocols ...string) *Client { c.oneTimeRegistry.Drivers = c.Registry.PreferProtocol(protocols...) c.oneTimeRegistryEnabled = true return c } ================================================ FILE: fixtures/internal/sum/ChangeBiosConfig ================================================ Supermicro Update Manager (for UEFI BIOS) 2.14.0 (2024/02/15) (ARM64) Copyright(C) 2013-2024 Super Micro Computer, Inc. All rights reserved. .................  Note: No BIOS setting has been changed. Status: The BIOS configuration is updated for 10.145.129.168 Note: You have to reboot or power up the system for the changes to take effect. ================================================ FILE: fixtures/internal/sum/ChangeBiosConfig-Changed ================================================ Supermicro Update Manager (for UEFI BIOS) 2.14.0 (2024/02/15) (ARM64) Copyright(C) 2013-2024 Super Micro Computer, Inc. All rights reserved. .................. Status: The BIOS configuration is updated for 10.145.129.168 Note: You have to reboot or power up the system for the changes to take effect. ================================================ FILE: fixtures/internal/sum/ChangeBiosConfig-Changed-Reboot ================================================ Supermicro Update Manager (for UEFI BIOS) 2.14.0 (2024/02/15) (ARM64) Copyright(C) 2013-2024 Super Micro Computer, Inc. All rights reserved. ................. Status: The managed system 10.145.129.168 is rebooting. .............................Done .... .................  Note: No BIOS setting has been changed. Status: The BIOS configuration is updated for 10.145.129.168 WARNING: Without option --post_complete, please manually confirm the managed system is POST complete before executing next action. ================================================ FILE: fixtures/internal/sum/GetBIOSInfo ================================================ Supermicro Update Manager (for UEFI BIOS) 2.14.0 (2024/02/15) (ARM64) Copyright(C) 2013-2024 Super Micro Computer, Inc. All rights reserved. .... Managed system..........................10.145.129.168 Board ID............................1B0F BIOS build date.....................2022/09/16 BIOS version........................1.9 ================================================ FILE: fixtures/internal/sum/GetBiosConfiguration ================================================ Supermicro Update Manager (for UEFI BIOS) 2.14.0 (2024/02/15) (ARM64) Copyright(C) 2013-2024 Super Micro Computer, Inc. All rights reserved. ....................... Supermicro X11SCM-F BIOS Version(1.9) Build Date(09/16/2022) CPLD Version(03.B3.05) Memory Information Total Memory(32768 MB) Disabled Checked On Force BIOS Enabled Disabled Disabled Last State Instant Off Disabled 32767 1 1 100 CPU Configuration Intel(R) Xeon(R) E-2278G CPU @ 3.40GHz() CPU Signature(0x906ED) Microcode Revision(F4) CPU Speed(3400 MHz) L1 Data Cache(32 KB x 8) L1 Instruction Cache(32 KB x 8) L2 Cache(256 KB x 8) L3 Cache(16 MB) L4 Cache(N/A) VMX(Supported) SMX/TXT(Supported) Disabled 63 0 0 20 Enabled Enabled Enabled All Enabled Disabled Enabled Max Non-Turbo Performance Enabled Disabled Enabled Disabled Enabled Enabled Enabled C1 and C3 C1 and C3 Disabled Disabled Enabled Auto Disabled 4095875 0 125 0 0 Enabled 4095875 0 125 0 Unchecked WARNING: Setting wrong values in below sections may cause system to malfunction. System Agent (SA) Configuration SA PCIe Code Version(7.0.88.80) VT-d(Supported) Memory Configuration Memory RC Version(0.7.1.115) Memory Frequency( 2667 MHz) Memory Timings (tCL-tRCD-tRP-tRAS)(19-19-19-43) DIMMA1(Not Present) DIMMA2(Populated & Enabled) Size(16384 MB (DDR4)) Number of Ranks(2) Manufacturer(Micron Technology) DIMMB1(Not Present) DIMMB2(Populated & Enabled) Size(16384 MB (DDR4)) Number of Ranks(2) Manufacturer(Micron Technology) Auto Dynamic Enabled Enabled Disabled DMI/OPI Configuration DMI(X4 Gen3) L1 Disabled -3.5 dB PEG Port Configuration CPU Slot6 PCI-E 3.0 X16(x8 Gen3) Auto Auto Auto Auto Both Root and Endpoint Ports -3.5 dB 255 0 1 75 1.0x 8191 0 1 1 Auto Disabled Enabled Software Controlled No Change in Owner EPOCHs 18446744073709551615 0 1 6142344250440060711 18446744073709551615 0 1 15521488688214965697 Unlocked 18446744073709551615 0 1 0 18446744073709551615 0 1 0 18446744073709551615 0 1 0 18446744073709551615 0 1 0 INVALID PRMRR Enabled Disabled PCH-IO Configuration PCI Express Configuration Auto Disabled Auto L1.1 & L1.2 Auto Auto L1.1 & L1.2 Auto Enabled Enabled Super IO Configuration Super IO Chip(AST2500) Serial Port 1 Configuration Checked Device Settings(IO=3F8h; IRQ=4;) Auto Serial Port 2 Configuration Checked Device Settings(IO=2F8h; IRQ=3;) Auto COM1 Unchecked COM1 Console Redirection Settings VT100+ 115200 8 None 1 None Checked Unchecked Checked VT100 SOL Checked SOL Console Redirection Settings VT100+ 115200 8 None 1 None Checked Unchecked Checked VT100 Legacy Console Redirection Legacy Console Redirection Settings COM1 80x25 Always Enable Serial Port for Out-of-Band Management/ Windows Emergency Management Services (EMS) Unchecked COM1 VT-UTF8 115200 None Data Bits(8) Parity(None) Stop Bits(1) SATA And RSTe Configuration Enabled AHCI MSI Disabled Legacy Serial ATA Port 0(Micron_5300_MT (480.1GB)) Software Preserve(SUPPORTED) Enabled Disabled Hard Disk Drive Serial ATA Port 1(Micron_5300_MT (480.1GB)) Software Preserve(SUPPORTED) Enabled Disabled Hard Disk Drive Serial ATA Port 2(Empty) Software Preserve(Unknown) Enabled Disabled Hard Disk Drive Serial ATA Port 3(Empty) Software Preserve(Unknown) Enabled Disabled Hard Disk Drive Serial ATA Port 4(Empty) Software Preserve(Unknown) Enabled Disabled Hard Disk Drive Serial ATA Port 5(Empty) Software Preserve(Unknown) Enabled Disabled Hard Disk Drive Serial ATA Port 6(Empty) Software Preserve(Unknown) Enabled Disabled Hard Disk Drive PCH-FW Configuration Operational Firmware Version(5.1.4.700) Backup Firmware Version(N/A) Recovery Firmware Version(5.1.4.700) ME Firmware Features(SiEn NM PECIProxy ICC MeStorageServices BootGuard PmBusProxy HSIO PCHDebug PowerThermalUtility PCHThermalSensorInit DeepSx DirectMeUpdate TelemetryHub ) ME Firmware Status #1(0x00000255) ME Firmware Status #2(0x89118027) Current State(Operational) Error Code(No Error) Enabled Enabled Enabled Auto Disabled USB Configuration USB Module Version(23) USB Controllers:() 1 XHCI USB Devices:() 1 Keyboard, 1 Mouse, 1 Hub Enabled Enabled Enabled Enabled Option ROM execution Disabled Disabled Disabled Onboard Legacy Vendor Defined Firmware PCIe/PCI/PnP Configuration Legacy Legacy Legacy Legacy PXE Disabled Enabled Enabled Disabled Enabled Disabled Disabled 5 0 1 0 50 1 1 1 TPM20 Device Found Firmware Version:(7.62) Vendor:(IFX) Enable Active PCR banks(SHA-1,SHA256) Available PCR banks(SHA-1,SHA256) Enabled Enabled None Enabled Enabled Enabled TCG_2 1.3 Enabled Auto Disabled Disabled HTTP BOOT Configuration Disabled 0 75 False 0 80 False to change the SMBIOS Event Log configuration.]]> Enabling/Disabling Options Enabled Disabled Erasing Settings No Do Nothing SMBIOS Event Log Standard Settings Disabled 255 1 1 1 99 0 1 60 to view the SMBIOS Event Log records.]]> DATE TIME ERROR CODE SEVERITY Password Description If the Administrator / User password is set, then this only limits access to Setup and is asked for when entering Setup. Please set Administrator password first in order for setting User password, if clear Administrator password, User password will be cleared as well. The password length must be in the following range: Minimum length(3) Maximum length(20) Setup Disabled Security Module Version(1.00) HDD Name(Micron_5300_MTFDDAK480TDT) HDD Serial Number(20422B887014) Security Mode(SAT3 Supported) Estimated Time(2 Minutes) HDD User Pwd Status(NOT INSTALLED) Disable 0 32 False HDD Name(Micron_5300_MTFDDAK480TDT) HDD Serial Number(20422B8885C5) Security Mode(SAT3 Supported) Estimated Time(2 Minutes) HDD User Pwd Status(NOT INSTALLED) Disable 0 32 False 3 20 False 3 20 False Disabled Disabled System Mode(Setup) Secure Boot(Not Active) Setup HDD Security Configuration: HDD Password Description : Allows Access to Set, Modify and Clear HDD PASSWORD CONFIGURATION: Security Supported :(Yes) Security Supported :(No) Security Enabled :(No) Security Locked :(Yes) Security Locked :(No) Security Frozen :(No) HDD User Pwd Status:(INSTALLED) HDD User Pwd Status:(NOT INSTALLED) HDD Master Pwd Status :(NOT INSTALLED) 0 32 False HDD Password Description : Allows Access to Set, Modify and Clear HDD PASSWORD CONFIGURATION: Security Supported :(No) Security Enabled :(Yes) Security Locked :(No) Security Frozen :(Yes) Security Frozen :(No) HDD User Pwd Status:(NOT INSTALLED) HDD Master Pwd Status :(INSTALLED) HDD Master Pwd Status :(NOT INSTALLED) 0 32 False Boot Configuration 65535 1 1 1 DUAL FIXED BOOT ORDER Priorities UEFI Hard Disk UEFI CD/DVD UEFI USB Hard Disk UEFI USB CD/DVD UEFI USB Key UEFI USB Floppy UEFI USB Lan UEFI Network UEFI AP:UEFI: Built-in EFI Shell Hard Disk: Micron_5300_MTFDDAK480TDT CD/DVD USB Hard Disk USB CD/DVD USB Key USB Floppy USB Lan Network:FlexBoot v3.5.901 (PCI 01:00.0) Hard Disk: Micron_5300_MTFDDAK480TDT CD/DVD USB Hard Disk USB CD/DVD USB Key USB Floppy USB Lan Network:FlexBoot v3.5.901 (PCI 01:00.0) UEFI Hard Disk UEFI CD/DVD UEFI USB Hard Disk UEFI USB CD/DVD UEFI USB Key UEFI USB Floppy UEFI USB Lan UEFI Network UEFI AP:UEFI: Built-in EFI Shell UEFI: Built-in EFI Shell P0: Micron_5300_MTFDDAK480TDT(SATA,Port:0) P1: Micron_5300_MTFDDAK480TDT(SATA,Port:1) FlexBoot v3.5.901 (PCI 01:00.0) FlexBoot v3.5.901 (PCI 01:00.1) ================================================ FILE: fixtures/internal/sum/SetBiosConfiguration ================================================ Supermicro Update Manager (for UEFI BIOS) 2.14.0 (2024/02/15) (ARM64) Copyright(C) 2013-2024 Super Micro Computer, Inc. All rights reserved. ................. Status: The managed system 10.145.129.168 is rebooting. .............................Done .... .................  Note: No BIOS setting has been changed. Status: The BIOS configuration is updated for 10.145.129.168 WARNING: Without option --post_complete, please manually confirm the managed system is POST complete before executing next action. ================================================ FILE: go.mod ================================================ module github.com/bmc-toolbox/bmclib/v2 go 1.21 require ( dario.cat/mergo v1.0.1 github.com/Jeffail/gabs/v2 v2.7.0 github.com/bmc-toolbox/common v0.0.0-20250112191656-b6de52e8303d github.com/bombsimon/logrusr/v2 v2.0.1 github.com/ghodss/yaml v1.0.0 github.com/go-logr/logr v1.4.2 github.com/go-logr/zerologr v1.2.3 github.com/google/go-cmp v0.6.0 github.com/hashicorp/go-multierror v1.1.1 github.com/jacobweinstock/iamt v0.0.0-20230502042727-d7cdbe67d9ef github.com/jacobweinstock/registrar v0.4.7 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.33.0 github.com/sirupsen/logrus v1.9.3 github.com/stmcginnis/gofish v0.21.6 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.29.0 go.opentelemetry.io/otel/trace v1.29.0 go.uber.org/goleak v1.3.0 golang.org/x/net v0.33.0 gopkg.in/go-playground/assert.v1 v1.2.1 ) require ( github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230 // indirect github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/satori/go.uuid v1.2.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Jeffail/gabs/v2 v2.7.0 h1:Y2edYaTcE8ZpRsR2AtmPu5xQdFDIthFG0jYhu5PY8kg= github.com/Jeffail/gabs/v2 v2.7.0/go.mod h1:dp5ocw1FvBBQYssgHsG7I1WYsiLRtkUaB1FEtSwvNUw= github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230 h1:t95Grn2mOPfb3+kPDWsNnj4dlNcxnvuR72IjY8eYjfQ= github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230/go.mod h1:t2EzW1qybnPDQ3LR/GgeF0GOzHUXT5IVMLP2gkW1cmc= github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22 h1:a0MBqYm44o0NcthLKCljZHe1mxlN6oahCQHHThnSwB4= github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22/go.mod h1:/B7V22rcz4860iDqstGvia/2+IYWXf3/JdQCVd/1D2A= github.com/bmc-toolbox/common v0.0.0-20250112191656-b6de52e8303d h1:5c0jhS9jNLm1t3GVEESsWv+p6recFRLGW90zp8HDIDs= github.com/bmc-toolbox/common v0.0.0-20250112191656-b6de52e8303d/go.mod h1:Cdnkm+edb6C0pVkyCrwh3JTXAe0iUF9diDG/DztPI9I= github.com/bombsimon/logrusr/v2 v2.0.1 h1:1VgxVNQMCvjirZIYaT9JYn6sAVGVEcNtRE0y4mvaOAM= github.com/bombsimon/logrusr/v2 v2.0.1/go.mod h1:ByVAX+vHdLGAfdroiMg6q0zgq2FODY2lc5YJvzmOJio= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v1.0.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/jacobweinstock/iamt v0.0.0-20230502042727-d7cdbe67d9ef h1:G4k02HGmBUfJFSNu3gfKJ+ki+B3qutKsYzYndkqqKc4= github.com/jacobweinstock/iamt v0.0.0-20230502042727-d7cdbe67d9ef/go.mod h1:FgmiLTU6cJewV4Xgrq6m5o8CUlTQOJtqzaFLGA0mG+E= github.com/jacobweinstock/registrar v0.4.7 h1:s4dOExccgD+Pc7rJC+f3Mc3D+NXHcXUaOibtcEsPxOc= github.com/jacobweinstock/registrar v0.4.7/go.mod h1:PWmkdGFG5/ZdCqgMo7pvB3pXABOLHc5l8oQ0sgmBNDU= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stmcginnis/gofish v0.21.6 h1:jK3TGD6VANaAHKHypVNfD6io2nPrU+6eF8X4qARsTlY= github.com/stmcginnis/gofish v0.21.6/go.mod h1:PzF5i8ecRG9A2ol8XT64npKUunyraJ+7t0kYMpQAtqU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: internal/executor/errors.go ================================================ package executor import ( "errors" "fmt" ) var ( ErrNoCommandOutput = errors.New("command returned no output") ErrVersionStrExpectedSemver = errors.New("expected version string to follow semver format") ErrFakeExecutorInvalidArgs = errors.New("invalid number of args passed to fake executor") ErrRepositoryBaseURL = errors.New("repository base URL undefined, ensure UpdateOptions.BaseURL OR UPDATE_BASE_URL env var is set") ErrNoUpdatesApplicable = errors.New("no updates applicable") ErrDmiDecodeRun = errors.New("error running dmidecode") ErrComponentListExpected = errors.New("expected a list of components to apply updates") ErrDeviceInventory = errors.New("failed to collect device inventory") ErrUnsupportedDiskVendor = errors.New("unsupported disk vendor") ErrNoUpdateHandlerForComponent = errors.New("component slug has no update handler declared") ErrBinNotExecutable = errors.New("bin has no executable bit set") ErrBinLstat = errors.New("failed to run lstat on bin") ErrBinLookupPath = errors.New("failed to lookup bin path") ) // ExecError is returned when the command exits with an error or a non zero exit status type ExecError struct { Cmd string Stderr string Stdout string ExitCode int } // Error implements the error interface func (u *ExecError) Error() string { return fmt.Sprintf("cmd %s exited with error: %s\n\t exitCode: %d\n\t stdout: %s", u.Cmd, u.Stderr, u.ExitCode, u.Stdout) } func newExecError(cmd string, r *Result) *ExecError { return &ExecError{ Cmd: cmd, Stderr: string(r.Stderr), Stdout: string(r.Stdout), ExitCode: r.ExitCode, } } ================================================ FILE: internal/executor/executor.go ================================================ package executor import ( "bytes" "context" "io" "os" "os/exec" "strings" "github.com/pkg/errors" ) // Executor interface lets us implement dummy executors for tests type Executor interface { ExecWithContext(context.Context) (*Result, error) SetArgs([]string) SetEnv([]string) GetCmd() string CheckExecutable() error SetStdout([]byte) } func NewExecutor(cmd string) Executor { return &Execute{Cmd: cmd, CheckBin: true} } // An execute instace type Execute struct { Cmd string Args []string Env []string Stdin io.Reader CheckBin bool Quiet bool } // The result of a command execution type Result struct { Stdout []byte Stderr []byte ExitCode int } // GetCmd returns the command with args as a string func (e *Execute) GetCmd() string { cmd := []string{e.Cmd} cmd = append(cmd, e.Args...) return strings.Join(cmd, " ") } // SetArgs sets the command args func (e *Execute) SetArgs(a []string) { e.Args = a } // SetEnv sets the env variables func (e *Execute) SetEnv(env []string) { e.Env = env } // SetStdout doesn't do much, is around for tests func (e *Execute) SetStdout(_ []byte) { } // ExecWithContext executes the command and returns the Result object func (e *Execute) ExecWithContext(ctx context.Context) (result *Result, err error) { if e.CheckBin { err = e.CheckExecutable() if err != nil { return nil, err } } cmd := exec.CommandContext(ctx, e.Cmd, e.Args...) cmd.Env = append(cmd.Env, e.Env...) cmd.Stdin = e.Stdin var stdoutBuf, stderrBuf bytes.Buffer if !e.Quiet { cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) } else { cmd.Stderr = &stderrBuf cmd.Stdout = &stdoutBuf } if err := cmd.Run(); err != nil { result = &Result{stdoutBuf.Bytes(), stderrBuf.Bytes(), cmd.ProcessState.ExitCode()} return result, newExecError(e.GetCmd(), result) } result = &Result{stdoutBuf.Bytes(), stderrBuf.Bytes(), cmd.ProcessState.ExitCode()} return result, nil } // CheckExecutable determines if the set Cmd value exists as a file and is an executable. func (e *Execute) CheckExecutable() error { var path string if strings.Contains(e.Cmd, "/") { path = e.Cmd } else { var err error path, err = exec.LookPath(e.Cmd) if err != nil { return errors.Wrap(ErrBinLookupPath, err.Error()) } e.Cmd = path } fileInfo, err := os.Lstat(path) if err != nil { return errors.Wrap(ErrBinLstat, err.Error()) } // bit mask 0111 indicates atleast one of owner, group, others has an executable bit set if fileInfo.Mode()&0o111 == 0 { return ErrBinNotExecutable } return nil } ================================================ FILE: internal/executor/executor_test.go ================================================ package executor import ( "bytes" "context" "fmt" "io/fs" "os" "testing" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) func Test_Stdin(t *testing.T) { e := new(Execute) e.Cmd = "grep" e.Args = []string{"hello"} e.Stdin = bytes.NewReader([]byte("hello")) result, err := e.ExecWithContext(context.Background()) if err != nil { fmt.Println(err.Error()) } assert.Equal(t, []byte("hello\n"), result.Stdout) } type checkBinTester struct { createFile bool filePath string expectedErr error fileMode uint testName string } func initCheckBinTests() []checkBinTester { return []checkBinTester{ { false, "f", ErrBinLookupPath, 0, "bin path lookup err test", }, { false, "/tmp/f", ErrBinLstat, 0, "bin exists err test", }, { true, "/tmp/f", ErrBinNotExecutable, 0o666, "bin exists with no executable bit test", }, { true, "/tmp/j", nil, 0o667, "bin with executable bit returns no error", }, { true, "/tmp/k", nil, 0o700, "bin with owner executable bit returns no error", }, { true, "/tmp/l", nil, 0o070, "bin with group executable bit returns no error", }, { true, "/tmp/m", nil, 0o007, "bin with other executable bit returns no error", }, } } func Test_CheckExecutable(t *testing.T) { tests := initCheckBinTests() for _, c := range tests { if c.createFile { f, err := os.Create(c.filePath) if err != nil { t.Error(err) } // nolint:gocritic // test code defer os.Remove(c.filePath) if c.fileMode != 0 { err = f.Chmod(fs.FileMode(c.fileMode)) if err != nil { t.Error(err) } } } e := new(Execute) e.Cmd = c.filePath err := e.CheckExecutable() assert.Equal(t, c.expectedErr, errors.Cause(err), c.testName) } } ================================================ FILE: internal/executor/fake_executor.go ================================================ package executor import ( "context" "io" "strings" ) // FakeExecute implements the utils.Executor interface // to enable testing type FakeExecute struct { Cmd string Args []string Env []string CheckBin bool Stdin io.Reader Stdout []byte // Set this for the dummy data to be returned Stderr []byte // Set this for the dummy data to be returned Quiet bool ExitCode int } func NewFakeExecutor(cmd string) Executor { return &FakeExecute{Cmd: cmd, CheckBin: false} } // nolint:gocyclo // TODO: break this method up and move into each $util_test.go // FakeExecute method returns whatever you want it to return // Set e.Stdout and e.Stderr to data to be returned func (e *FakeExecute) ExecWithContext(_ context.Context) (*Result, error) { // switch e.Cmd { // case "ipmicfg": // if e.Args[0] == "-summary" { // buf := new(bytes.Buffer) // _, err := buf.ReadFrom(e.Stdin) // if err != nil { // return nil, err // } // e.Stdout = buf.Bytes() // } // } return &Result{Stdout: e.Stdout, Stderr: e.Stderr, ExitCode: 0}, nil } // CheckExecutable implements the Executor interface func (e *FakeExecute) CheckExecutable() error { return nil } // CmdPath returns the absolute path to the executable // this means the caller should not have disabled CheckBin. func (e *FakeExecute) CmdPath() string { return e.Cmd } func (e *FakeExecute) SetArgs(a []string) { e.Args = a } func (e *FakeExecute) SetEnv(env []string) { e.Env = env } func (e *FakeExecute) SetQuiet() { e.Quiet = true } func (e *FakeExecute) SetVerbose() { e.Quiet = false } func (e *FakeExecute) SetStdout(b []byte) { e.Stdout = b } func (e *FakeExecute) SetStderr(b []byte) { e.Stderr = b } func (e *FakeExecute) SetStdin(r io.Reader) { e.Stdin = r } func (e *FakeExecute) DisableBinCheck() { e.CheckBin = false } func (e *FakeExecute) SetExitCode(i int) { e.ExitCode = i } func (e *FakeExecute) GetCmd() string { cmd := []string{e.Cmd} cmd = append(cmd, e.Args...) return strings.Join(cmd, " ") } ================================================ FILE: internal/helper/helper.go ================================================ package helper import ( "regexp" "runtime" ) var basename = regexp.MustCompile(`^.+\.(.*$)`) // WhosCalling returns the current caller of the functions func WhosCalling() string { pc, _, _, ok := runtime.Caller(1) if ok { return basename.ReplaceAllString(runtime.FuncForPC(pc).Name(), "${1}") } return "unknown" } ================================================ FILE: internal/helper/helper_test.go ================================================ package helper import "testing" func TestWhosCalling(t *testing.T) { expectedAnswer := "TestWhosCalling" answer := WhosCalling() if answer != expectedAnswer { t.Errorf("Expected answer %v: found %v", expectedAnswer, answer) } } ================================================ FILE: internal/httpclient/httpclient.go ================================================ package httpclient import ( "crypto/tls" "crypto/x509" "net" "net/http" "net/http/cookiejar" "strings" "time" "golang.org/x/net/publicsuffix" ) // SecureTLS disables InsecureSkipVerify and adds a cert pool to an HTTP client's // TLS config func SecureTLS(c *http.Client, rootCAs *x509.CertPool) { if c == nil { return } tp := DefaultTransport() if c.Transport != nil { if assertedTransport, ok := c.Transport.(*http.Transport); ok { tp = assertedTransport } // otherwise, we overwrite the transport } tp.TLSClientConfig.InsecureSkipVerify = false tp.TLSClientConfig.RootCAs = rootCAs c.Transport = tp } // DefaultTransport sets an HTTP Transport func DefaultTransport() *http.Transport { return &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, DisableKeepAlives: true, Dial: (&net.Dialer{ Timeout: 120 * time.Second, KeepAlive: 120 * time.Second, }).Dial, TLSHandshakeTimeout: 120 * time.Second, ResponseHeaderTimeout: 120 * time.Second, } } // SecureTLSOption disables InsecureSkipVerify and adds a cert pool to an HTTP client's // TLS config func SecureTLSOption(rootCAs *x509.CertPool) func(*http.Client) { return func(c *http.Client) { SecureTLS(c, rootCAs) } } // Build builds a client session with our default parameters func Build(opts ...func(*http.Client)) *http.Client { // we ignore the error here because cookiejar.New always returns nil jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) client := &http.Client{ Timeout: time.Second * 120, Transport: DefaultTransport(), Jar: jar, } for _, opt := range opts { if opt != nil { opt(client) } } return client } // StandardizeProcessorName makes the processor name standard across vendors func StandardizeProcessorName(name string) string { return strings.ToLower(strings.TrimSuffix(strings.TrimSpace(strings.Split(name, "@")[0]), " 0")) } ================================================ FILE: internal/httpclient/httpclient_test.go ================================================ package httpclient import ( "crypto/x509" "fmt" "net/http" "net/http/httptest" "net/url" "testing" ) func CertPoolFromCert(cert *x509.Certificate) *x509.CertPool { certPool := x509.NewCertPool() certPool.AddCert(cert) return certPool } func TestBuildWithOptions(t *testing.T) { cases := []struct { name string secureClient bool withCertPool func(cert *x509.Certificate) *x509.CertPool wantErr bool }{ { "Default not secure, no error", false, func(_ *x509.Certificate) *x509.CertPool { return nil }, false, }, { "Default secure, want an error", true, func(_ *x509.Certificate) *x509.CertPool { return nil }, true, }, { "Default secure, no error", true, CertPoolFromCert, false, }, } server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{"hello": "client"}`) })) for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { opts := []func(*http.Client){} if tc.secureClient { opts = append(opts, SecureTLSOption(tc.withCertPool(server.Certificate()))) } client := Build(opts...) req, _ := http.NewRequest(http.MethodGet, server.URL, nil) _, err := client.Do(req) if tc.wantErr { if err == nil { t.Fatal("Missing expected error") } // Different versions of Go return different error messages so we just // check that its a *url.Error{} if _, ok := err.(*url.Error); !ok { t.Fatalf("Missing expected error: got %T: '%s'", err, err) } return } if err != nil { t.Fatalf("Got unexpected error %s", err) } }) } } ================================================ FILE: internal/ipmi/ipmi.go ================================================ package ipmi import ( "bufio" "context" "fmt" "net" "os" "os/exec" "strings" "github.com/go-logr/logr" "github.com/pkg/errors" ) // Ipmi holds the date for an ipmi connection type Ipmi struct { Username string Password string Host string ipmitool string cipherSuite string log logr.Logger } // Option for setting optional Ipmi values type Option func(*Ipmi) func WithIpmitoolPath(path string) Option { return func(i *Ipmi) { i.ipmitool = path } } func WithCipherSuite(cipherSuite string) Option { return func(i *Ipmi) { i.cipherSuite = cipherSuite } } func WithLogger(log logr.Logger) Option { return func(i *Ipmi) { i.log = log } } // New returns a new ipmi instance func New(username string, password string, host string, opts ...Option) (ipmi *Ipmi, err error) { ipmi = &Ipmi{ Username: username, Password: password, Host: host, log: logr.Discard(), } for _, opt := range opts { opt(ipmi) } if ipmi.ipmitool == "" { ipmi.ipmitool, err = exec.LookPath("ipmitool") if err != nil { return nil, err } } else { if _, err = os.Stat(ipmi.ipmitool); err != nil { return nil, err } } return ipmi, err } func (i *Ipmi) run(ctx context.Context, command []string) (output string, err error) { var out []byte var ipmiCiphers = []string{"3", "17"} ipmiArgs := []string{"-I", "lanplus", "-U", i.Username, "-E", "-N", "5"} if strings.Contains(i.Host, ":") { host, port, err := net.SplitHostPort(i.Host) if err == nil { ipmiArgs = append(ipmiArgs, "-H", host, "-p", port) } } else { ipmiArgs = append(ipmiArgs, "-H", i.Host) } if i.cipherSuite != "" { ipmiCiphers = []string{i.cipherSuite} } for _, cipherString := range ipmiCiphers { ipmiCmd := append(ipmiArgs, "-C", cipherString) i.log.V(3).Info("ipmitool options", "opts", formatOptions(ipmiCmd)) ipmiCmd = append(ipmiCmd, command...) cmd := exec.CommandContext(ctx, i.ipmitool, ipmiCmd...) cmd.Env = []string{fmt.Sprintf("IPMITOOL_PASSWORD=%s", i.Password)} out, err = cmd.CombinedOutput() if err == nil || ctx.Err() != nil { break } } if ctx.Err() == context.DeadlineExceeded { return string(out), ctx.Err() } return string(out), errors.Wrap(err, strings.TrimSpace(string(out))) } type cmdOpt struct { Opt string `json:"opt"` Val string `json:"val"` } func formatOptions(opts []string) []cmdOpt { result := []cmdOpt{} for _, opt := range opts { if strings.HasPrefix(opt, "-") { o := cmdOpt{Opt: opt} if opt == "-E" { o.Val = "-E" } result = append(result, o) } else { result[len(result)-1].Val = opt } } return result } // PowerCycle reboots the machine via bmc func (i *Ipmi) PowerCycle(ctx context.Context) (status bool, err error) { output, err := i.run(ctx, []string{"chassis", "power", "cycle"}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if strings.HasPrefix(output, "Chassis Power Control: Cycle") { return true, err } return false, fmt.Errorf("%v: %v", err, output) } // ForceRestart does the chassis power cycle even if the chassis is turned off. // From the RedFish spec (https://www.dmtf.org/sites/default/files/standards/documents/DSP2046_2018.1.pdf): // // Perform an immediate (non-graceful) shutdown, followed by a restart. func (i *Ipmi) ForceRestart(ctx context.Context) (status bool, err error) { output, err := i.run(ctx, []string{"chassis", "power", "status"}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } command := "on" reply := "Up/On" if strings.HasPrefix(output, "Chassis Power is on") { command = "cycle" reply = "Cycle" } else if !strings.HasPrefix(output, "Chassis Power is off") { return false, fmt.Errorf("%v: %v", err, output) } output, err = i.run(ctx, []string{"chassis", "power", command}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if strings.HasPrefix(output, "Chassis Power Control: "+reply) { return true, err } return false, fmt.Errorf("%v: %v", err, output) } // PowerReset reboots the machine via bmc func (i *Ipmi) PowerReset(ctx context.Context) (status bool, err error) { output, err := i.run(ctx, []string{"chassis", "power", "reset"}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if !strings.HasPrefix(output, "Chassis Power Control: Reset") { return false, fmt.Errorf("%v: %v", err, output) } return true, err } // PowerCycleBmc reboots the bmc we are connected to func (i *Ipmi) PowerCycleBmc(ctx context.Context) (status bool, err error) { output, err := i.run(ctx, []string{"mc", "reset", "cold"}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if strings.HasPrefix(output, "Sent cold reset command to MC") { return true, err } return false, fmt.Errorf("%v: %v", err, output) } // PowerResetBmc reboots the bmc we are connected to func (i *Ipmi) PowerResetBmc(ctx context.Context, resetType string) (ok bool, err error) { output, err := i.run(ctx, []string{"mc", "reset", strings.ToLower(resetType)}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if strings.HasPrefix(output, fmt.Sprintf("Sent %v reset command to MC", strings.ToLower(resetType))) { return true, err } return false, fmt.Errorf("%v: %v", err, output) } // PowerOn power on the machine via bmc func (i *Ipmi) PowerOn(ctx context.Context) (status bool, err error) { s, err := i.IsOn(ctx) if err != nil { return false, errors.Wrap(err, "error checking power state") } if s { return true, nil } output, err := i.run(ctx, []string{"chassis", "power", "on"}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if strings.HasPrefix(output, "Chassis Power Control: Up/On") { return true, nil } return false, fmt.Errorf("stderr/stdout: %v", output) } // PowerOnForce power on the machine via bmc even when the machine is already on (Thanks HP!) func (i *Ipmi) PowerOnForce(ctx context.Context) (status bool, err error) { output, err := i.run(ctx, []string{"chassis", "power", "on"}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if strings.HasPrefix(output, "Chassis Power Control: Up/On") { return true, err } return false, fmt.Errorf("%v: %v", err, output) } // PowerOff power off the machine via bmc func (i *Ipmi) PowerOff(ctx context.Context) (status bool, err error) { if on, err := i.IsOn(ctx); err == nil && !on { return true, nil } output, err := i.run(ctx, []string{"chassis", "power", "off"}) if strings.Contains(output, "Chassis Power Control: Down/Off") { return true, err } return false, fmt.Errorf("%v: %v", err, output) } // PowerSoft power off the machine via bmc func (i *Ipmi) PowerSoft(ctx context.Context) (status bool, err error) { on, _ := i.IsOn(ctx) if !on { return true, nil } output, err := i.run(ctx, []string{"chassis", "power", "soft"}) if !strings.Contains(output, "Chassis Power Control: Soft") { return false, fmt.Errorf("%v: %v", err, output) } return true, err } // PxeOnceEfi makes the machine to boot via pxe once using EFI func (i *Ipmi) PxeOnceEfi(ctx context.Context) (status bool, err error) { output, err := i.run(ctx, []string{"chassis", "bootdev", "pxe", "options=efiboot"}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if strings.Contains(output, "Set Boot Device to pxe") { return true, err } return false, fmt.Errorf("%v: %v", err, output) } // BootDeviceSet sets the next boot device with options func (i *Ipmi) BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { var atLeastOneOptionSelected bool ipmiCmd := []string{"chassis", "bootdev", strings.ToLower(bootDevice)} var opts []string if setPersistent { opts = append(opts, "persistent") atLeastOneOptionSelected = true } if efiBoot { opts = append(opts, "efiboot") atLeastOneOptionSelected = true } if atLeastOneOptionSelected { optsJoined := strings.Join(opts, ",") optsFull := fmt.Sprintf("options=%v", optsJoined) ipmiCmd = append(ipmiCmd, optsFull) } output, err := i.run(ctx, ipmiCmd) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if strings.Contains(output, fmt.Sprintf("Set Boot Device to %v", strings.ToLower(bootDevice))) { return true, err } return false, fmt.Errorf("%v: %v", err, output) } // PxeOnceMbr makes the machine to boot via pxe once using MBR func (i *Ipmi) PxeOnceMbr(ctx context.Context) (status bool, err error) { output, err := i.run(ctx, []string{"chassis", "bootdev", "pxe"}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if strings.Contains(output, "Set Boot Device to pxe") { return true, err } return false, fmt.Errorf("%v: %v", err, output) } // PxeOnce makes the machine to boot via pxe once using MBR func (i *Ipmi) PxeOnce(ctx context.Context) (status bool, err error) { return i.PxeOnceMbr(ctx) } // IsOn tells if a machine is currently powered on func (i *Ipmi) IsOn(ctx context.Context) (status bool, err error) { output, err := i.run(ctx, []string{"chassis", "power", "status"}) if err != nil { return false, fmt.Errorf("%v: %v", err, output) } if strings.Contains(output, "Chassis Power is on") { return true, err } return false, err } // PowerState returns the current power state of the machine func (i *Ipmi) PowerState(ctx context.Context) (state string, err error) { return i.run(ctx, []string{"chassis", "power", "status"}) } // ReadUsers list all BMC users func (i *Ipmi) ReadUsers(ctx context.Context) (users []map[string]string, err error) { output, err := i.run(ctx, []string{"user", "list"}) if err != nil { return users, errors.Wrap(err, "error getting user list") } header := map[int]string{} firstLine := true scanner := bufio.NewScanner(strings.NewReader(output)) for scanner.Scan() { line := strings.Fields(scanner.Text()) if firstLine { firstLine = false for x := 0; x < 5; x++ { header[x] = line[x] } continue } entry := map[string]string{} if line[1] != "true" { for x := 0; x < 5; x++ { entry[header[x]] = line[x] } users = append(users, entry) } } return users, err } // ClearSystemEventLog clears the system event log func (i *Ipmi) ClearSystemEventLog(ctx context.Context) (err error) { _, err = i.run(ctx, []string{"sel", "clear"}) return err } // GetSystemEventLog returns the system event log entries in ID, Timestamp, Description, Message format func (i *Ipmi) GetSystemEventLog(ctx context.Context) (entries [][]string, err error) { output, err := i.GetSystemEventLogRaw(ctx) if err != nil { return nil, errors.Wrap(err, "error getting system event log") } entries = parseSystemEventLog(output) return entries, nil } // parseSystemEventLogRaw parses the raw output of the system event log. Helper // function for GetSystemEventLog to make testing the parser easier. func parseSystemEventLog(raw string) (entries [][]string) { scanner := bufio.NewScanner(strings.NewReader(raw)) for scanner.Scan() { line := strings.Split(scanner.Text(), "|") if len(line) < 6 { continue } if line[0] == "ID" { continue } for i := range line { line[i] = strings.TrimSpace(line[i]) } // ID, Timestamp (date time), Description, Message (message : assertion) entries = append(entries, []string{line[0], fmt.Sprintf("%s %s", line[1], line[2]), line[3], fmt.Sprintf("%s : %s", line[4], line[5])}) } return entries } // GetSystemEventLogRaw returns the raw SEL output func (i *Ipmi) GetSystemEventLogRaw(ctx context.Context) (eventlog string, err error) { output, err := i.run(ctx, []string{"sel", "list"}) if err != nil { return "", errors.Wrap(err, "error getting system event log") } return output, nil } func (i *Ipmi) DeactivateSOL(ctx context.Context) (err error) { out, err := i.run(ctx, []string{"sol", "deactivate"}) // Don't treat this as a failure (we just want to ensure there // isn't an active SOL session left open) if strings.TrimSpace(out) == "Info: SOL payload already de-activated" { err = nil } return err } // SendPowerDiag tells the BMC to issue an NMI to the device func (i *Ipmi) SendPowerDiag(ctx context.Context) error { _, err := i.run(ctx, []string{"chassis", "power", "diag"}) if err != nil { err = errors.Wrap(err, "failed sending power diag") } return err } ================================================ FILE: internal/redfishwrapper/bios.go ================================================ package redfishwrapper import ( "context" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/stmcginnis/gofish/schemas" ) func (c *Client) GetBiosConfiguration(ctx context.Context) (biosConfig map[string]string, err error) { sys, err := c.System() if err != nil { return nil, err } biosConfig = make(map[string]string) if !c.compatibleOdataID(sys.ODataID, knownSystemsOdataIDs) { return biosConfig, nil } bios, err := sys.Bios() if err != nil { return nil, err } if bios == nil { return nil, bmclibErrs.ErrNoBiosAttributes } for attr := range bios.Attributes { biosConfig[attr] = bios.Attributes.String(attr) } return biosConfig, nil } func (c *Client) SetBiosConfiguration(ctx context.Context, biosConfig map[string]string) (err error) { sys, err := c.System() if err != nil { return err } settingsAttributes := make(schemas.SettingsAttributes) for attr, value := range biosConfig { settingsAttributes[attr] = value } if !c.compatibleOdataID(sys.ODataID, knownSystemsOdataIDs) { return nil } bios, err := sys.Bios() if err != nil { return err } // TODO(jwb) We should handle passing different apply times here return bios.UpdateBiosAttributesApplyAt(settingsAttributes, schemas.OnResetSettingsApplyTime) } func (c *Client) ResetBiosConfiguration(ctx context.Context) (err error) { sys, err := c.System() if err != nil { return err } if !c.compatibleOdataID(sys.ODataID, knownSystemsOdataIDs) { return nil } bios, err := sys.Bios() if err != nil { return err } _, err = bios.ResetBios() return err } ================================================ FILE: internal/redfishwrapper/bios_test.go ================================================ package redfishwrapper import ( "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "testing" "github.com/stretchr/testify/assert" ) func biosConfigFromFixture(t *testing.T) map[string]string { t.Helper() fixturePath := fixturesDir + "/dell/bios.json" fh, err := os.Open(fixturePath) if err != nil { t.Fatalf("%s, failed to open fixture: %s", err.Error(), fixturePath) } defer fh.Close() b, err := io.ReadAll(fh) if err != nil { t.Fatalf("%s, failed to read fixture: %s", err.Error(), fixturePath) } var bios map[string]any err = json.Unmarshal([]byte(b), &bios) if err != nil { t.Fatalf("%s, failed to unmarshal fixture: %s", err.Error(), fixturePath) } expectedBiosConfig := make(map[string]string) for k, v := range bios["Attributes"].(map[string]any) { expectedBiosConfig[k] = fmt.Sprintf("%v", v) } return expectedBiosConfig } func TestGetBiosConfiguration(t *testing.T) { tests := []struct { testName string hfunc map[string]func(http.ResponseWriter, *http.Request) expectedBiosConfig map[string]string }{ { "GetBiosConfiguration", map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "/dell/serviceroot.json"), "/redfish/v1/Systems": endpointFunc(t, "/dell/systems.json"), "/redfish/v1/Systems/System.Embedded.1": endpointFunc(t, "/dell/system.embedded.1.json"), "/redfish/v1/Systems/System.Embedded.1/Bios": endpointFunc(t, "/dell/bios.json"), }, biosConfigFromFixture(t), }, } for _, tc := range tests { t.Run(tc.testName, func(t *testing.T) { mux := http.NewServeMux() handleFunc := tc.hfunc for endpoint, handler := range handleFunc { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } ctx := context.Background() client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "", WithBasicAuthEnabled(true)) err = client.Open(ctx) if err != nil { t.Fatal(err) } biosConfig, err := client.GetBiosConfiguration(ctx) assert.Nil(t, err) assert.Equal(t, tc.expectedBiosConfig, biosConfig) }) } } ================================================ FILE: internal/redfishwrapper/boot_device.go ================================================ package redfishwrapper import ( "context" "github.com/bmc-toolbox/bmclib/v2/bmc" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" "github.com/stmcginnis/gofish/schemas" ) type bootDeviceMapping struct { BootDeviceType bmc.BootDeviceType RedFishTarget schemas.BootSource } var bootDeviceTypeMappings = []bootDeviceMapping{ { BootDeviceType: bmc.BootDeviceTypeBIOS, RedFishTarget: schemas.BiosSetupBootSource, }, { BootDeviceType: bmc.BootDeviceTypeCDROM, RedFishTarget: schemas.CdBootSource, }, { BootDeviceType: bmc.BootDeviceTypeDiag, RedFishTarget: schemas.DiagsBootSource, }, { BootDeviceType: bmc.BootDeviceTypeFloppy, RedFishTarget: schemas.FloppyBootSource, }, { BootDeviceType: bmc.BootDeviceTypeDisk, RedFishTarget: schemas.HddBootSource, }, { BootDeviceType: bmc.BootDeviceTypeNone, RedFishTarget: schemas.NoneBootSource, }, { BootDeviceType: bmc.BootDeviceTypePXE, RedFishTarget: schemas.PxeBootSource, }, { BootDeviceType: bmc.BootDeviceTypeRemoteDrive, RedFishTarget: schemas.RemoteDriveBootSource, }, { BootDeviceType: bmc.BootDeviceTypeSDCard, RedFishTarget: schemas.SDCardBootSource, }, { BootDeviceType: bmc.BootDeviceTypeUSB, RedFishTarget: schemas.UsbBootSource, }, { BootDeviceType: bmc.BootDeviceTypeUtil, RedFishTarget: schemas.UtilitiesBootSource, }, { BootDeviceType: bmc.BootDeviceUefiHTTP, RedFishTarget: schemas.UefiHTTPBootSource, }, } // bootDeviceStringToTarget gets the RedFish BootSource that corresponds to the given device string, // or an error if the device is not a RedFish BootSource. func bootDeviceStringToTarget(device string) (schemas.BootSource, error) { for _, bootDevice := range bootDeviceTypeMappings { if string(bootDevice.BootDeviceType) == device { return bootDevice.RedFishTarget, nil } } return "", errors.New("invalid boot device") } // bootTargetToBootDeviceType converts the redfish boot target to a bmc.BootDeviceType. // if the target is unknown or unsupported, then an error is returned. func bootTargetToBootDeviceType(target schemas.BootSource) (bmc.BootDeviceType, error) { for _, bootDevice := range bootDeviceTypeMappings { if bootDevice.RedFishTarget == target { return bootDevice.BootDeviceType, nil } } return "", errors.New("invalid boot device") } // SystemBootDeviceSet set the boot device for the system. func (c *Client) SystemBootDeviceSet(_ context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { if err := c.SessionActive(); err != nil { return false, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } system, err := c.System() if err != nil { return false, err } boot := system.Boot boot.BootSourceOverrideTarget, err = bootDeviceStringToTarget(bootDevice) if err != nil { return false, err } if setPersistent { boot.BootSourceOverrideEnabled = schemas.ContinuousBootSourceOverrideEnabled } else { boot.BootSourceOverrideEnabled = schemas.OnceBootSourceOverrideEnabled } if efiBoot { boot.BootSourceOverrideMode = schemas.UEFIBootSourceOverrideMode } else { boot.BootSourceOverrideMode = schemas.LegacyBootSourceOverrideMode } if err = system.SetBoot(&boot); err != nil { // Some redfish implementations don't like all the fields we're setting so we // try again here with a minimal set of fields. This has shown to work with the // Redfish implementation on HP DL160 Gen10. secondTry := schemas.Boot{} secondTry.BootSourceOverrideTarget = boot.BootSourceOverrideTarget secondTry.BootSourceOverrideEnabled = boot.BootSourceOverrideEnabled if err = system.SetBoot(&secondTry); err != nil { return false, err } } return true, nil } // GetBootDeviceOverride returns the current boot override settings func (c *Client) GetBootDeviceOverride(_ context.Context) (override bmc.BootDeviceOverride, err error) { if err := c.SessionActive(); err != nil { return override, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } system, err := c.System() if err != nil { return override, err } boot := system.Boot bootDevice, err := bootTargetToBootDeviceType(boot.BootSourceOverrideTarget) if err != nil { return override, err } override = bmc.BootDeviceOverride{ IsPersistent: boot.BootSourceOverrideEnabled == schemas.ContinuousBootSourceOverrideEnabled, IsEFIBoot: boot.BootSourceOverrideMode == schemas.UEFIBootSourceOverrideMode, Device: bootDevice, } return override, nil } ================================================ FILE: internal/redfishwrapper/client.go ================================================ package redfishwrapper import ( "context" "crypto/x509" "fmt" "io" "net/http" "os" "slices" "strconv" "strings" "time" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/go-logr/logr" "github.com/pkg/errors" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/schemas" ) var ( ErrManagerID = errors.New("error identifying Manager Odata ID") ErrBIOSID = errors.New("error identifying System BIOS Odata ID") ) // Client is a redfishwrapper client which wraps the gofish client. type Client struct { host string port string user string pass string systemName string basicAuth bool disableEtagMatch bool versionsNotCompatible []string // a slice of redfish versions to ignore as incompatible client *gofish.APIClient httpClient *http.Client httpClientSetupFuncs []func(*http.Client) logger logr.Logger } // Option is a function applied to a *Conn type Option func(*Client) // WithHTTPClient returns an option that sets an HTTP client for the connecion func WithHTTPClient(cli *http.Client) Option { return func(c *Client) { c.httpClient = cli } } // WithSecureTLS returns an option that enables secure TLS with an optional cert pool. func WithSecureTLS(rootCAs *x509.CertPool) Option { return func(c *Client) { c.httpClientSetupFuncs = append(c.httpClientSetupFuncs, httpclient.SecureTLSOption(rootCAs)) } } // WithVersionsNotCompatible returns an option that sets the redfish versions to ignore as incompatible. // // The version string value must match the value returned by // curl -k "https://10.247.133.39/redfish/v1" | jq .RedfishVersion func WithVersionsNotCompatible(versions []string) Option { return func(c *Client) { c.versionsNotCompatible = append(c.versionsNotCompatible, versions...) } } // WithBasicAuthEnabled sets Basic Auth on the Gofish driver. func WithBasicAuthEnabled(e bool) Option { return func(c *Client) { c.basicAuth = e } } // WithEtagMatchDisabled disables the If-Match Etag header from being included by the Gofish driver. // // As of the current implementation this disables the header for POST/PATCH requests to the System entity endpoints. func WithEtagMatchDisabled(d bool) Option { return func(c *Client) { c.disableEtagMatch = d } } // WithLogger sets the logger on the redfish wrapper client func WithLogger(l *logr.Logger) Option { return func(c *Client) { c.logger = *l } } func WithSystemName(name string) Option { return func(c *Client) { c.systemName = name } } // NewClient returns a redfishwrapper client func NewClient(host, port, user, pass string, opts ...Option) *Client { if !strings.HasPrefix(host, "https://") && !strings.HasPrefix(host, "http://") { host = "https://" + host } client := &Client{ host: host, port: port, user: user, pass: pass, logger: logr.Discard(), versionsNotCompatible: []string{}, } for _, opt := range opts { opt(client) } return client } // Open sets up a new redfish session. func (c *Client) Open(ctx context.Context) error { endpoint := c.host if c.port != "" { endpoint = c.host + ":" + c.port } config := gofish.ClientConfig{ Endpoint: endpoint, Username: c.user, Password: c.pass, Insecure: true, HTTPClient: c.httpClient, BasicAuth: c.basicAuth, } if config.HTTPClient == nil { config.HTTPClient = httpclient.Build(c.httpClientSetupFuncs...) } else { for _, setupFunc := range c.httpClientSetupFuncs { setupFunc(config.HTTPClient) } } debug := os.Getenv("DEBUG_BMCLIB") if debug == "true" { config.DumpWriter = os.Stdout } if tm := getTimeout(ctx); tm != 0 { config.HTTPClient.Timeout = tm } var err error c.client, err = gofish.Connect(config) return err } func getTimeout(ctx context.Context) time.Duration { deadline, ok := ctx.Deadline() if !ok { return 0 } return time.Until(deadline) } // Close closes the redfish session. func (c *Client) Close(ctx context.Context) error { if c.client == nil || c.client.Service == nil { return nil } c.client.Logout() return nil } // SessionActive returns an error if a redfish session is not active. func (c *Client) SessionActive() error { if c.client == nil || c.client.Service == nil { return bmclibErrs.ErrNotAuthenticated } // With basic auth enabled theres no session to be checked. if c.basicAuth { return nil } _, err := c.client.GetSession() if err != nil { return err } return nil } // Overrides the HTTP client timeout func (c *Client) SetHttpClientTimeout(t time.Duration) { c.client.HTTPClient.Timeout = t } // retrieve the current HTTP client timeout func (c *Client) HttpClientTimeout() time.Duration { return c.client.HTTPClient.Timeout } // RunRawRequestWithHeaders wraps the gofish client method RunRawRequestWithHeaders func (c *Client) RunRawRequestWithHeaders(method, url string, payloadBuffer io.ReadSeeker, contentType string, customHeaders map[string]string) (*http.Response, error) { if err := c.SessionActive(); err != nil { return nil, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } return c.client.RunRawRequestWithHeaders(method, url, payloadBuffer, contentType, customHeaders) } func (c *Client) Delete(url string) (*http.Response, error) { return c.client.Delete(url) } func (c *Client) Get(url string) (*http.Response, error) { return c.client.Get(url) } // VersionCompatible compares the redfish version reported by the BMC with the blacklist if specified. func (c *Client) VersionCompatible() bool { if len(c.versionsNotCompatible) == 0 { return true } if err := c.SessionActive(); err != nil { return false } return !slices.Contains(c.versionsNotCompatible, c.client.Service.RedfishVersion) } // redfishVersionMeetsOrExceeds compares this connection's redfish version to what is provided // as a requirement. We rely on the stated structure of the version string as described in the // Protocol Version (section 6.6) of the Redfish spec. If an implementation's version string is // non-conforming this function returns false. func redfishVersionMeetsOrExceeds(version string, major, minor, patch int) bool { if version == "" { return false } parts := strings.Split(version, ".") if len(parts) != 3 { return false } var rfVer []int64 for _, part := range parts { ver, err := strconv.ParseInt(part, 10, 32) if err != nil { return false } rfVer = append(rfVer, ver) } if rfVer[0] < int64(major) { return false } if rfVer[1] < int64(minor) { return false } return rfVer[2] >= int64(patch) } func (c *Client) GetBootProgress() ([]*schemas.BootProgress, error) { // The redfish standard adopts the BootProgress object in 1.13.0. Earlier versions of redfish return // json NULL, which gofish turns into a zero-value object of BootProgress. We gate this on the RedfishVersion // to avoid the complexity of interpreting whether a given value is legitimate. if !redfishVersionMeetsOrExceeds(c.client.Service.RedfishVersion, 1, 13, 0) { return nil, fmt.Errorf("%w: %s", bmclibErrs.ErrRedfishVersionIncompatible, c.client.Service.RedfishVersion) } systems, err := c.client.Service.Systems() if err != nil { return nil, fmt.Errorf("retrieving redfish systems collection: %w", err) } bps := []*schemas.BootProgress{} for _, sys := range systems { bps = append(bps, &sys.BootProgress) } return bps, nil } func (c *Client) PostWithHeaders(ctx context.Context, url string, payload interface{}, headers map[string]string) (*http.Response, error) { return c.client.PostWithHeaders(url, payload, headers) } func (c *Client) PatchWithHeaders(ctx context.Context, url string, payload interface{}, headers map[string]string) (*http.Response, error) { return c.client.PatchWithHeaders(url, payload, headers) } func (c *Client) Tasks(ctx context.Context) ([]*schemas.Task, error) { ts, err := c.client.Service.Tasks() if err != nil { return []*schemas.Task{}, err } return ts.Tasks() } func (c *Client) ManagerOdataID(ctx context.Context) (string, error) { managers, err := c.client.Service.Managers() if err != nil { return "", errors.Wrap(ErrManagerID, err.Error()) } for _, m := range managers { if m.ID != "" { return m.ODataID, nil } } return "", ErrManagerID } func (c *Client) SystemsBIOSOdataID(ctx context.Context) (string, error) { systems, err := c.client.Service.Systems() if err != nil { return "", errors.Wrap(ErrBIOSID, err.Error()) } for _, s := range systems { bios, err := s.Bios() if err != nil { return "", errors.Wrap(ErrBIOSID, err.Error()) } if bios == nil { return "", ErrBIOSID } if bios.ID != "" { return bios.ODataID, nil } } return "", ErrBIOSID } // DeviceVendorModel returns the device manufacturer and model attributes func (c *Client) DeviceVendorModel(ctx context.Context) (vendor, model string, err error) { systems, err := c.client.Service.Systems() if err != nil { return "", "", err } for _, sys := range systems { return sys.Manufacturer, sys.Model, nil } return vendor, model, bmclibErrs.ErrSystemVendorModel } ================================================ FILE: internal/redfishwrapper/client_test.go ================================================ package redfishwrapper import ( "context" "net/http" "net/http/httptest" "net/url" "testing" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/stmcginnis/gofish/schemas" "github.com/stretchr/testify/assert" ) func TestWithVersionsNotCompatible(t *testing.T) { host := "127.0.0.1" user := "ADMIN" pass := "ADMIN" tests := []struct { name string versions []string }{ { "no versions", []string{}, }, { "with versions", []string{"1.2.3", "4.5.6"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := NewClient(host, "", user, pass, WithVersionsNotCompatible(tt.versions)) assert.Equal(t, tt.versions, client.versionsNotCompatible) }) } } func TestWithBasicAuthEnabled(t *testing.T) { host := "127.0.0.1" user := "ADMIN" pass := "ADMIN" tests := []struct { name string enabled bool }{ { "disabled", false, }, { "enabled", true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := NewClient(host, "", user, pass, WithBasicAuthEnabled(tt.enabled)) assert.Equal(t, tt.enabled, client.basicAuth) }) } } func TestWithEtagMatchDisabled(t *testing.T) { host := "127.0.0.1" user := "ADMIN" pass := "ADMIN" tests := []struct { name string disabled bool }{ { "disabled", true, }, { "enabled", false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := NewClient(host, "", user, pass, WithEtagMatchDisabled(tt.disabled)) assert.Equal(t, tt.disabled, client.disableEtagMatch) }) } } const ( fixturesDir = "./fixtures" ) func TestManagerOdataID(t *testing.T) { tests := map[string]struct { hfunc map[string]func(http.ResponseWriter, *http.Request) expect string err error }{ "happy case": { hfunc: map[string]func(http.ResponseWriter, *http.Request){ // service root "/redfish/v1/": endpointFunc(t, "serviceroot.json"), "/redfish/v1/Systems": endpointFunc(t, "systems.json"), "/redfish/v1/Managers": endpointFunc(t, "managers.json"), "/redfish/v1/Managers/1": endpointFunc(t, "managers_1.json"), }, expect: "/redfish/v1/Managers/1", err: nil, }, "failure case": { hfunc: map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "/serviceroot_no_manager.json"), }, expect: "", err: ErrManagerID, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { mux := http.NewServeMux() handleFunc := tc.hfunc for endpoint, handler := range handleFunc { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } ctx := context.Background() //os.Setenv("DEBUG_BMCLIB", "true") client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "") err = client.Open(ctx) if err != nil { t.Fatal(err) } got, err := client.ManagerOdataID(ctx) if err != nil { assert.Equal(t, tc.err, err) } assert.Equal(t, tc.expect, got) client.Close(context.Background()) }) } } func TestSystemsBIOSOdataID(t *testing.T) { tests := map[string]struct { hfunc map[string]func(http.ResponseWriter, *http.Request) expect string err error }{ "happy case": { hfunc: map[string]func(http.ResponseWriter, *http.Request){ // service root "/redfish/v1/": endpointFunc(t, "serviceroot.json"), "/redfish/v1/Systems": endpointFunc(t, "systems.json"), "/redfish/v1/Systems/1": endpointFunc(t, "systems_1.json"), "/redfish/v1/Systems/1/Bios": endpointFunc(t, "systems_bios.json"), }, expect: "/redfish/v1/Systems/1/Bios", err: nil, }, "failure case": { hfunc: map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "serviceroot.json"), }, expect: "", err: ErrBIOSID, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { mux := http.NewServeMux() handleFunc := tc.hfunc for endpoint, handler := range handleFunc { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } ctx := context.Background() client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "") err = client.Open(ctx) if err != nil { t.Fatal(err) } got, err := client.SystemsBIOSOdataID(ctx) if err != nil { assert.Equal(t, tc.err, err) } assert.Equal(t, tc.expect, got) client.Close(context.Background()) }) } } func TestRedfishVersionMeetsOrExceeds(t *testing.T) { t.Parallel() testCases := []struct { name string version string exp bool }{ { "empty string", "", false, }, { "short string", "1.2", false, }, { "bogus component", "1.asdf.2", false, }, { "major too low", "0.3.4", false, }, { "minor too low", "1.1.3", false, }, { "patch too low", "1.2.2", false, }, { "meets", "1.2.3", true, }, { "exceeds", "1.2.4", true, }, } for _, tc := range testCases { got := redfishVersionMeetsOrExceeds(tc.version, 1, 2, 3) assert.Equal(t, tc.exp, got, "testcase %s", tc.name) } } func TestGetBootProgress(t *testing.T) { tests := map[string]struct { hfunc map[string]func(http.ResponseWriter, *http.Request) expect []*schemas.BootProgress err error }{ "happy case": { hfunc: map[string]func(http.ResponseWriter, *http.Request){ // service root "/redfish/v1/": endpointFunc(t, "smc_1.14.0_serviceroot.json"), "/redfish/v1/Systems": endpointFunc(t, "smc_1.14.0_systems.json"), "/redfish/v1/Systems/1": endpointFunc(t, "smc_1.14.0_systems_1.json"), }, expect: []*schemas.BootProgress{ &schemas.BootProgress{ LastState: schemas.SystemHardwareInitializationCompleteBootProgressTypes, }, }, err: nil, }, "insufficient redfish version": { hfunc: map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "smc_1.9.0_serviceroot.json"), }, expect: nil, err: bmclibErrs.ErrRedfishVersionIncompatible, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { mux := http.NewServeMux() handleFunc := tc.hfunc for endpoint, handler := range handleFunc { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "") err = client.Open(context.TODO()) if err != nil { t.Fatal(err) } defer client.Close(context.TODO()) got, err := client.GetBootProgress() if err != nil { assert.ErrorIs(t, err, tc.err) return } assert.ElementsMatch(t, tc.expect, got) }) } } ================================================ FILE: internal/redfishwrapper/firmware.go ================================================ package redfishwrapper import ( "bytes" "context" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/textproto" "os" "path/filepath" "strconv" "strings" "time" "github.com/pkg/errors" "github.com/stmcginnis/gofish/schemas" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" ) type installMethod string const ( unstructuredHttpPush installMethod = "unstructuredHttpPush" multipartHttpUpload installMethod = "multipartUpload" ) var ( // the URI for starting a firmware update via StartUpdate is defined in the Redfish Resource and // Schema Guide (2024.1) startUpdateURI = "/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate" ) var ( errMultiPartPayload = errors.New("error preparing multipart payload") errUpdateParams = errors.New("error in redfish UpdateParameters payload") errTaskIdFromRespBody = errors.New("failed to identify firmware install taskID from response body") ) type RedfishUpdateServiceParameters struct { Targets []string `json:"Targets"` OperationApplyTime constants.OperationApplyTime `json:"@Redfish.OperationApplyTime"` Oem json.RawMessage `json:"Oem"` } // FirmwareUpload uploads and initiates the firmware install process func (c *Client) FirmwareUpload(ctx context.Context, updateFile *os.File, params *RedfishUpdateServiceParameters) (taskID string, err error) { parameters, err := json.Marshal(params) if err != nil { return "", errors.Wrap(errUpdateParams, err.Error()) } installMethod, installURI, err := c.firmwareInstallMethodURI() if err != nil { return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, err.Error()) } // override the gofish HTTP client timeout, // since the context timeout is set at Open() and is at a lower value than required for this operation. // // record the http client timeout to be restored when this method returns httpClientTimeout := c.HttpClientTimeout() defer func() { c.SetHttpClientTimeout(httpClientTimeout) }() ctxDeadline, _ := ctx.Deadline() c.SetHttpClientTimeout(time.Until(ctxDeadline)) var resp *http.Response switch installMethod { case multipartHttpUpload: var uploadErr error resp, uploadErr = c.multipartHTTPUpload(installURI, updateFile, parameters) if uploadErr != nil { return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, uploadErr.Error()) } case unstructuredHttpPush: var uploadErr error resp, uploadErr = c.unstructuredHttpUpload(installURI, updateFile) if uploadErr != nil { return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, uploadErr.Error()) } default: return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, "unsupported install method: "+string(installMethod)) } response, err := io.ReadAll(resp.Body) if err != nil { return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusAccepted { return "", errors.Wrap( bmclibErrs.ErrFirmwareUpload, "unexpected status code returned: "+resp.Status, ) } // The response contains a location header pointing to the task URI // Location: /redfish/v1/TaskService/Tasks/JID_467696020275 var location = resp.Header.Get("Location") if strings.Contains(location, "/TaskService/Tasks/") { return taskIDFromLocationHeader(location) } rfTask := &schemas.Task{} if err := rfTask.UnmarshalJSON(response); err != nil { // we got invalid JSON return "", fmt.Errorf("unmarshaling redfish response: %w", err) } // it's possible to get well-formed JSON that isn't a Task (thanks SMC). Test that we have something sensible. if strings.Contains(rfTask.ODataType, "Task") { return rfTask.ID, nil } return taskIDFromResponseBody(response) } // StartUpdateForUploadedFirmware starts an update for a firmware file previously uploaded and returns the taskID func (c *Client) StartUpdateForUploadedFirmware(ctx context.Context) (taskID string, err error) { errStartUpdate := errors.New("error in starting update for uploaded firmware") updateService, err := c.client.Service.UpdateService() if err != nil { return "", errors.Wrap(err, "error querying redfish update service") } // Start update the hard way. We do this to get back the task object from the response body so that // we can parse the task id out of it. resp, err := updateService.GetClient().PostWithHeaders(startUpdateURI, nil, nil) if err != nil { return "", errors.Wrap(err, "error querying redfish start update endpoint") } response, err := io.ReadAll(resp.Body) if err != nil { return "", errors.Wrap(err, "error reading redfish start update response body") } defer resp.Body.Close() if resp.StatusCode != http.StatusAccepted { return "", errors.Wrap(errStartUpdate, "unexpected status code returned: "+resp.Status) } var location = resp.Header.Get("Location") if strings.Contains(location, "/TaskService/Tasks/") { return taskIDFromLocationHeader(location) } rfTask := &schemas.Task{} if err := rfTask.UnmarshalJSON(response); err != nil { // we got invalid JSON return "", fmt.Errorf("unmarshaling redfish response: %w", err) } if strings.Contains(rfTask.ODataType, "Task") { return rfTask.ID, nil } return taskIDFromResponseBody(response) } type TaskAccepted struct { Accepted struct { Code string `json:"code"` Message string `json:"Message"` MessageExtendedInfo []struct { MessageID string `json:"MessageId"` Severity string `json:"Severity"` Resolution string `json:"Resolution"` Message string `json:"Message"` MessageArgs []string `json:"MessageArgs"` RelatedProperties []string `json:"RelatedProperties"` } `json:"@Message.ExtendedInfo"` } `json:"Accepted"` } func taskIDFromResponseBody(resp []byte) (taskID string, err error) { a := &TaskAccepted{} if err = json.Unmarshal(resp, a); err != nil { return "", errors.Wrap(errTaskIdFromRespBody, err.Error()) } var taskURI string for _, info := range a.Accepted.MessageExtendedInfo { for _, msg := range info.MessageArgs { if !strings.Contains(msg, "/TaskService/Tasks/") { continue } taskURI = msg break } } if taskURI == "" { return "", errors.Wrap(errTaskIdFromRespBody, "TaskService/Tasks/ URI not identified") } tokens := strings.Split(taskURI, "/") if len(tokens) == 0 { return "", errors.Wrap(errTaskIdFromRespBody, "invalid/unsupported task URI: "+taskURI) } return tokens[len(tokens)-1], nil } func taskIDFromLocationHeader(uri string) (taskID string, err error) { uri = strings.TrimSuffix(uri, "/") switch { // OpenBMC returns /redfish/v1/TaskService/Tasks/12/Monitor case strings.Contains(uri, "/Tasks/") && strings.HasSuffix(uri, "/Monitor"): taskIDPart := strings.Split(uri, "/Tasks/")[1] taskID := strings.TrimSuffix(taskIDPart, "/Monitor") return taskID, nil case strings.Contains(uri, "Tasks/"): taskIDPart := strings.Split(uri, "/Tasks/")[1] return taskIDPart, nil default: return "", errors.Wrap(bmclibErrs.ErrTaskNotFound, "failed to parse taskID from uri: "+uri) } } type multipartPayload struct { updateParameters []byte updateFile *os.File } func (c *Client) multipartHTTPUpload(url string, update *os.File, params []byte) (*http.Response, error) { if url == "" { return nil, fmt.Errorf("unable to execute request, no target provided") } // payload ordered in the format it ends up in the multipart form payload := &multipartPayload{ updateParameters: params, updateFile: update, } return c.runRequestWithMultipartPayload(url, payload) } func (c *Client) unstructuredHttpUpload(url string, update io.Reader) (*http.Response, error) { if url == "" { return nil, fmt.Errorf("unable to execute request, no target provided") } // TODO: transform this to read the update so that we don't hold the data in memory b, _ := io.ReadAll(update) payloadReadSeeker := bytes.NewReader(b) return c.RunRawRequestWithHeaders(http.MethodPost, url, payloadReadSeeker, "application/octet-stream", nil) } // firmwareUpdateMethodURI returns the updateMethod and URI func (c *Client) firmwareInstallMethodURI() (method installMethod, updateURI string, err error) { updateService, err := c.UpdateService() if err != nil { return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, err.Error()) } // update service disabled if !updateService.ServiceEnabled { return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "service disabled") } switch { case updateService.MultipartHTTPPushURI != "": return multipartHttpUpload, updateService.MultipartHTTPPushURI, nil case updateService.HTTPPushURI != "": //nolint:staticcheck return unstructuredHttpPush, updateService.HTTPPushURI, nil //nolint:staticcheck } return "", "", errors.Wrap(bmclibErrs.ErrRedfishUpdateService, "unsupported update method") } // sets up the UpdateParameters MIMEHeader for the multipart form // the Go multipart writer CreateFormField does not currently let us set Content-Type on a MIME Header // https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/mime/multipart/writer.go;l=151 func updateParametersFormField(fieldName string, writer *multipart.Writer) (io.Writer, error) { if fieldName != "UpdateParameters" { return nil, errors.Wrap(errUpdateParams, "expected field not found to create multipart form") } h := make(textproto.MIMEHeader) h.Set("Content-Disposition", `form-data; name="UpdateParameters"`) h.Set("Content-Type", "application/json") return writer.CreatePart(h) } // pipeReaderFakeSeeker wraps the io.PipeReader and implements the io.Seeker interface // to meet the API requirements for the Gofish client https://github.com/stmcginnis/gofish/blob/46b1b33645ed1802727dc4df28f5d3c3da722b15/client.go#L434 // // The Gofish method linked does not currently perform seeks and so a PR will be suggested // to change the method signature to accept an io.Reader instead. type pipeReaderFakeSeeker struct { *io.PipeReader } // Seek impelements the io.Seeker interface only to panic if called func (p pipeReaderFakeSeeker) Seek(offset int64, whence int) (int64, error) { return 0, errors.New("Seek() not implemented for fake pipe reader seeker.") } // multipartPayloadSize prepares a temporary multipart form to determine the form size // // It creates a temporary form without reading in the update file payload and returns // sizeOf(form) + sizeOf(update file) func multipartPayloadSize(payload *multipartPayload) (int64, *bytes.Buffer, error) { body := &bytes.Buffer{} form := multipart.NewWriter(body) // Add UpdateParameters field part part, err := updateParametersFormField("UpdateParameters", form) if err != nil { return 0, body, err } if _, err = io.Copy(part, bytes.NewReader(payload.updateParameters)); err != nil { return 0, body, err } // Add updateFile form _, err = form.CreateFormFile("UpdateFile", filepath.Base(payload.updateFile.Name())) if err != nil { return 0, body, err } // determine update file size finfo, err := payload.updateFile.Stat() if err != nil { return 0, body, err } // add terminating boundary to multipart form err = form.Close() if err != nil { return 0, body, err } return int64(body.Len()) + finfo.Size(), body, nil } // runRequestWithMultipartPayload is a copy of https://github.com/stmcginnis/gofish/blob/main/client.go#L349 // with a change to add the UpdateParameters multipart form field with a json content type header // the resulting form ends up in this format // // Content-Length: 416 // Content-Type: multipart/form-data; boundary=-------------------- // ----1771f60800cb2801 // --------------------------1771f60800cb2801 // Content-Disposition: form-data; name="UpdateParameters" // Content-Type: application/json // {"Targets": [], "@Redfish.OperationApplyTime": "OnReset", "Oem": // {}} // --------------------------1771f60800cb2801 // Content-Disposition: form-data; name="UpdateFile"; filename="dum // myfile" // Content-Type: application/octet-stream // hey. // --------------------------1771f60800cb2801-- func (c *Client) runRequestWithMultipartPayload(url string, payload *multipartPayload) (*http.Response, error) { if url == "" { return nil, fmt.Errorf("unable to execute request, no target provided") } // A content-length header is passed in to indicate the payload size // // The Content-length is set explicitly since the payload is an io.Reader, // https://github.com/golang/go/blob/ddad9b618cce0ed91d66f0470ddb3e12cfd7eeac/src/net/http/request.go#L861 // // Without the content-length header the http client will set the Transfer-Encoding to 'chunked' // and that does not work for some BMCs (iDracs). contentLength, _, err := multipartPayloadSize(payload) if err != nil { return nil, errors.Wrap(err, "error determining multipart payload size") } headers := map[string]string{ "Content-Length": strconv.FormatInt(contentLength, 10), } // setup pipe pipeReader, pipeWriter := io.Pipe() defer pipeReader.Close() // initiate a mulitpart writer form := multipart.NewWriter(pipeWriter) // go routine blocks on the io.Copy until the http request is made go func() { var err error defer func() { if err != nil { c.logger.Error(err, "multipart upload error occurred") } }() defer pipeWriter.Close() // Add UpdateParameters part parametersPart, err := updateParametersFormField("UpdateParameters", form) if err != nil { c.logger.Error(errMultiPartPayload, err.Error()+": UpdateParameters part copy error") return } if _, err = io.Copy(parametersPart, bytes.NewReader(payload.updateParameters)); err != nil { c.logger.Error(errMultiPartPayload, err.Error()+": UpdateParameters part copy error") return } // Add UpdateFile part updateFilePart, err := form.CreateFormFile("UpdateFile", filepath.Base(payload.updateFile.Name())) if err != nil { c.logger.Error(errMultiPartPayload, err.Error()+": UpdateFile part create error") return } if _, err = io.Copy(updateFilePart, payload.updateFile); err != nil { c.logger.Error(errMultiPartPayload, err.Error()+": UpdateFile part copy error") return } // add terminating boundary to multipart form form.Close() }() // pipeReader wrapped as a io.ReadSeeker to satisfy the gofish method signature reader := pipeReaderFakeSeeker{pipeReader} return c.RunRawRequestWithHeaders(http.MethodPost, url, reader, form.FormDataContentType(), headers) } ================================================ FILE: internal/redfishwrapper/firmware_test.go ================================================ package redfishwrapper import ( "bytes" "context" "encoding/json" "io" "log" "mime/multipart" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "testing" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/stretchr/testify/assert" "go.uber.org/goleak" ) func TestRunRequestWithMultipartPayload(t *testing.T) { defer goleak.VerifyNone(t) // init things tmpdir := t.TempDir() binPath := filepath.Join(tmpdir, "test.bin") err := os.WriteFile(binPath, []byte(`HELLOWORLD`), 0600) if err != nil { t.Fatal(err) } updateFile, err := os.Open(binPath) if err != nil { t.Fatalf("%s -> %s", err.Error(), binPath) } defer updateFile.Close() defer os.Remove(binPath) multipartEndpoint := func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusNotFound) } body, err := io.ReadAll(r.Body) if err != nil { log.Fatal(err) } // payload size expectedContentLength := "476" expected := []string{ `Content-Disposition: form-data; name="UpdateParameters"`, `Content-Type: application/json`, `{"Targets":[],"@Redfish.OperationApplyTime":"OnReset","Oem":{}}`, `Content-Disposition: form-data; name="UpdateFile"; filename="test.bin"`, `Content-Type: application/octet-stream`, `HELLOWORLD`, } for _, want := range expected { assert.Contains(t, string(body), want, "expected value in payload") } assert.Equal(t, expectedContentLength, r.Header.Get("Content-Length")) w.Header().Add("Location", "/redfish/v1/TaskService/Tasks/JID_467696020275") w.WriteHeader(http.StatusAccepted) } tests := map[string]struct { hfunc map[string]func(http.ResponseWriter, *http.Request) updateURI string payload *multipartPayload err error }{ "happy case - multipart push": { hfunc: map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "serviceroot.json"), "/redfish/v1/UpdateService/MultipartUpload": multipartEndpoint, }, updateURI: "/redfish/v1/UpdateService/MultipartUpload", payload: &multipartPayload{ updateParameters: []byte(`{"Targets":[],"@Redfish.OperationApplyTime":"OnReset","Oem":{}}`), updateFile: updateFile, }, err: nil, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { mux := http.NewServeMux() handleFunc := tc.hfunc for endpoint, handler := range handleFunc { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } ctx := context.Background() client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "", WithBasicAuthEnabled(true)) err = client.Open(ctx) if err != nil { t.Fatal(err) } _, err = client.runRequestWithMultipartPayload(tc.updateURI, tc.payload) if tc.err != nil { assert.ErrorContains(t, err, tc.err.Error()) return } assert.Nil(t, err) client.Close(context.Background()) }) } } func TestFirmwareInstallMethodURI(t *testing.T) { tests := map[string]struct { hfunc map[string]func(http.ResponseWriter, *http.Request) expectInstallMethod installMethod expectUpdateURI string err error }{ "happy case - multipart push": { hfunc: map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "serviceroot.json"), "/redfish/v1/Systems": endpointFunc(t, "systems.json"), "/redfish/v1/Managers": endpointFunc(t, "managers.json"), "/redfish/v1/Managers/1": endpointFunc(t, "managers_1.json"), "/redfish/v1/UpdateService": endpointFunc(t, "updateservice_with_multipart.json"), }, expectInstallMethod: multipartHttpUpload, expectUpdateURI: "/redfish/v1/UpdateService/MultipartUpload", err: nil, }, "happy case - unstructured http push": { hfunc: map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "serviceroot.json"), "/redfish/v1/Systems": endpointFunc(t, "systems.json"), "/redfish/v1/Managers": endpointFunc(t, "managers.json"), "/redfish/v1/Managers/1": endpointFunc(t, "managers_1.json"), "/redfish/v1/UpdateService": endpointFunc(t, "updateservice_with_httppushuri.json"), }, expectInstallMethod: unstructuredHttpPush, expectUpdateURI: "/redfish/v1/UpdateService/update", err: nil, }, "failure case - service disabled": { hfunc: map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "serviceroot.json"), "/redfish/v1/Systems": endpointFunc(t, "systems.json"), "/redfish/v1/Managers": endpointFunc(t, "managers.json"), "/redfish/v1/Managers/1": endpointFunc(t, "managers_1.json"), "/redfish/v1/UpdateService": endpointFunc(t, "updateservice_disabled.json"), }, expectInstallMethod: "", expectUpdateURI: "", err: bmclibErrs.ErrRedfishUpdateService, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { mux := http.NewServeMux() handleFunc := tc.hfunc for endpoint, handler := range handleFunc { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } ctx := context.Background() client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "", WithBasicAuthEnabled(true)) err = client.Open(ctx) if err != nil { t.Fatal(err) } gotMethod, gotURI, err := client.firmwareInstallMethodURI() if tc.err != nil { assert.ErrorContains(t, err, tc.err.Error()) return } assert.Nil(t, err) assert.Equal(t, tc.expectInstallMethod, gotMethod) assert.Equal(t, tc.expectUpdateURI, gotURI) client.Close(context.Background()) }) } } func TestTaskIDFromResponseBody(t *testing.T) { testCases := []struct { name string body []byte expectedID string expectedErr error }{ { name: "happy case", body: mustReadFile(t, "updateservice_ok_response.json"), expectedID: "1234", expectedErr: nil, }, { name: "failure case", body: mustReadFile(t, "updateservice_unexpected_response.json"), expectedID: "", expectedErr: errTaskIdFromRespBody, }, { name: "failure case - invalid json", body: []byte(`crappy bmc is crappy`), expectedID: "", expectedErr: errTaskIdFromRespBody, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { taskID, err := taskIDFromResponseBody(tc.body) if tc.expectedErr != nil { assert.ErrorContains(t, err, tc.expectedErr.Error()) return } assert.Nil(t, err) assert.Equal(t, tc.expectedID, taskID) }) } } func TestTaskIDFromLocationHeader(t *testing.T) { testCases := []struct { name string uri string expectedID string expectedErr error }{ { name: "task URI with JID", uri: "http://foo/redfish/v1/TaskService/Tasks/JID_12345", expectedID: "JID_12345", expectedErr: nil, }, { name: "task URI with ID", uri: "http://foo/redfish/v1/TaskService/Tasks/1234", expectedID: "1234", expectedErr: nil, }, { name: "task URI with Monitor suffix", uri: "/redfish/v1/TaskService/Tasks/12/Monitor", expectedID: "12", expectedErr: nil, }, { name: "trailing slash removed", uri: "http://foo/redfish/v1/TaskService/Tasks/1/", expectedID: "1", expectedErr: nil, }, { name: "invalid task URI - no task ID", uri: "http://foo/redfish/v1/TaskService/Tasks/", expectedID: "", expectedErr: bmclibErrs.ErrTaskNotFound, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { taskID, err := taskIDFromLocationHeader(tc.uri) if tc.expectedErr != nil { assert.ErrorContains(t, err, tc.expectedErr.Error()) return } assert.Nil(t, err) assert.Equal(t, tc.expectedID, taskID) }) } } func TestUpdateParametersFormField(t *testing.T) { testCases := []struct { name string fieldName string expectedErr error }{ { name: "happy case", fieldName: "UpdateParameters", expectedErr: nil, }, { name: "failure case", fieldName: "InvalidField", expectedErr: errUpdateParams, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { buf := new(bytes.Buffer) writer := multipart.NewWriter(buf) output, err := updateParametersFormField(tc.fieldName, writer) if tc.expectedErr != nil { assert.ErrorContains(t, err, tc.expectedErr.Error()) return } assert.NoError(t, err) assert.Contains(t, buf.String(), `Content-Disposition: form-data; name="UpdateParameters`) assert.Contains(t, buf.String(), `Content-Type: application/json`) assert.NotNil(t, output) // Validate the created multipart form content err = writer.Close() assert.NoError(t, err) }) } } func TestMultipartPayloadSize(t *testing.T) { updateParameters, err := json.Marshal(struct { Targets []string `json:"Targets"` RedfishOpApplyTime string `json:"@Redfish.OperationApplyTime"` Oem struct{} `json:"Oem"` }{ []string{}, "foobar", struct{}{}, }) if err != nil { t.Fatal(err) } tmpdir := t.TempDir() binPath := filepath.Join(tmpdir, "test.bin") err = os.WriteFile(binPath, []byte(`HELLOWORLD`), 0600) if err != nil { t.Fatal(err) } testfileFH, err := os.Open(binPath) if err != nil { t.Fatalf("%s -> %s", err.Error(), binPath) } testCases := []struct { testName string payload *multipartPayload expectedSize int64 errorMsg string }{ { "content length as expected", &multipartPayload{ updateParameters: updateParameters, updateFile: testfileFH, }, 475, "", }, } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { gotSize, _, err := multipartPayloadSize(tc.payload) if tc.errorMsg != "" { assert.Contains(t, err.Error(), tc.errorMsg) } assert.Nil(t, err) assert.Equal(t, tc.expectedSize, gotSize) }) } } ================================================ FILE: internal/redfishwrapper/fixtures/dell/bios.json ================================================ { "@odata.context": "/redfish/v1/$metadata#Bios.Bios", "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Bios", "@odata.type": "#Bios.v1_1_0.Bios", "Id": "Bios", "Name": "BIOS Configuration Current Settings", "Description": "BIOS Configuration Current Settings", "AttributeRegistry": "BiosAttributeRegistry.v1_0_3", "Attributes": { "SystemModelName": "PowerEdge R6515", "SystemBiosVersion": "2.2.4", "SystemServiceTag": "4PN08J3", "SystemManufacturer": "Dell Inc.", "SysMfrContactInfo": "www.dell.com", "SystemCpldVersion": "1.0.7", "UefiComplianceVersion": "2.7", "AgesaVersion": "RomePI-SP3 1.0.0.A", "SmuVersion": "0.36.109.0", "DxioVersion": "36.637", "ProcCoreSpeed": "2.80 GHz", "Proc1Id": "17-31-0", "Proc1Brand": "AMD EPYC 7402P 24-Core Processor ", "Proc1L2Cache": "24x512 KB", "Proc1L3Cache": "128 MB", "Proc1Microcode": "0x830104D", "SataPortAModel": "Not Enumerated", "SataPortADriveType": "Not Enumerated", "SataPortACapacity": "N/A", "SataPortBModel": "Not Enumerated", "SataPortBDriveType": "Not Enumerated", "SataPortBCapacity": "N/A", "SataPortCModel": "Not Enumerated", "SataPortCDriveType": "Not Enumerated", "SataPortCCapacity": "N/A", "SataPortDModel": "Not Enumerated", "SataPortDDriveType": "Not Enumerated", "SataPortDCapacity": "N/A", "SetBootOrderEn": "NIC.Slot.3-1-1,HardDisk.List.1-1", "SetBootOrderDis": "", "SetBootOrderFqdd1": "", "SetBootOrderFqdd2": "", "SetBootOrderFqdd3": "", "SetBootOrderFqdd4": "", "SetBootOrderFqdd5": "", "SetBootOrderFqdd6": "", "SetBootOrderFqdd7": "", "SetBootOrderFqdd8": "", "SetBootOrderFqdd9": "", "SetBootOrderFqdd10": "", "SetBootOrderFqdd11": "", "SetBootOrderFqdd12": "", "SetBootOrderFqdd13": "", "SetBootOrderFqdd14": "", "SetBootOrderFqdd15": "", "SetBootOrderFqdd16": "", "SetLegacyHddOrderFqdd1": "", "SetLegacyHddOrderFqdd2": "", "SetLegacyHddOrderFqdd3": "", "SetLegacyHddOrderFqdd4": "", "SetLegacyHddOrderFqdd5": "", "SetLegacyHddOrderFqdd6": "", "SetLegacyHddOrderFqdd7": "", "SetLegacyHddOrderFqdd8": "", "SetLegacyHddOrderFqdd9": "", "SetLegacyHddOrderFqdd10": "", "SetLegacyHddOrderFqdd11": "", "SetLegacyHddOrderFqdd12": "", "SetLegacyHddOrderFqdd13": "", "SetLegacyHddOrderFqdd14": "", "SetLegacyHddOrderFqdd15": "", "SetLegacyHddOrderFqdd16": "", "CurrentEmbVideoState": "Enabled", "AesNi": "Enabled", "TpmInfo": "Type: 2.0 NTC", "TpmFirmware": "1.3.2.8", "SysMemSize": "64 GB", "SysMemType": "ECC DDR4", "SysMemSpeed": "3200 MT/s", "SysMemVolt": "1.20 V", "VideoMem": "16 MB", "AssetTag": "", "SHA256SystemPassword": "", "SHA256SystemPasswordSalt": "", "SHA256SetupPassword": "", "SHA256SetupPasswordSalt": "", "CpuMinSevAsid": 1, "Proc1NumCores": 24, "ControlledTurboMinusBin": 0, "AcPwrRcvryUserDelay": 60, "LogicalProc": "Enabled", "ProcVirtualization": "Enabled", "IommuSupport": "Enabled", "L1StreamHwPrefetcher": "Enabled", "L2StreamHwPrefetcher": "Enabled", "MadtCoreEnumeration": "Linear", "NumaNodesPerSocket": "1", "CcxAsNumaDomain": "Disabled", "TransparentSme": "Disabled", "ProcX2Apic": "Enabled", "ProcCcds": "All", "CcdCores": "All", "ControlledTurbo": "Disabled", "OptimizerMode": "Auto", "EmbSata": "Off", "SecurityFreezeLock": "Disabled", "WriteCache": "Disabled", "SataPortA": "Auto", "SataPortB": "Auto", "SataPortC": "Auto", "SataPortD": "Auto", "NvmeMode": "NonRaid", "BiosNvmeDriver": "DellQualifiedDrives", "BootMode": "Bios", "BootSeqRetry": "Enabled", "HddFailover": "Enabled", "GenericUsbBoot": "Disabled", "HddPlaceholder": "Disabled", "SysPrepClean": "None", "OneTimeBootMode": "Disabled", "OneTimeBootSeqDev": "NIC.Slot.3-1-1", "OneTimeHddSeqDev": "AHCI.Slot.2-1", "UsbPorts": "AllOn", "InternalUsb": "On", "UsbManagedPort": "On", "IntegratedRaid": "Enabled", "EmbNic1Nic2": "DisabledOs", "EmbVideo": "Enabled", "PciePreferredIoBus": "Disabled", "PcieEnhancedPreferredIo": "Disabled", "SriovGlobalEnable": "Disabled", "OsWatchdogTimer": "Disabled", "MmioLimit": "8TB", "DellAutoDiscovery": "PlatformDefault", "Slot2Bif": "x16", "Slot3Bif": "x16", "Slot1": "Enabled", "Slot2": "Enabled", "Slot3": "Enabled", "SerialComm": "OnConRedirCom1", "SerialPortAddress": "Serial1Com1Serial2Com2", "ExtSerialConnector": "Serial1", "FailSafeBaud": "115200", "ConTermType": "Vt100Vt220", "RedirAfterBoot": "Enabled", "SysProfile": "PerfPerWattOptimizedOs", "ProcPwrPerf": "OsDbpm", "MemFrequency": "MaxPerf", "ProcTurboMode": "Enabled", "ProcCStates": "Enabled", "WriteDataCrc": "Disabled", "MemPatrolScrub": "Standard", "MemRefreshRate": "1x", "WorkloadProfile": "NotAvailable", "PcieAspmL1": "Enabled", "DeterminismSlider": "PowerDeterminism", "EfficiencyOptimizedMode": "Disabled", "ApbDis": "Disabled", "PasswordStatus": "Unlocked", "TpmSecurity": "On", "Tpm2Hierarchy": "Enabled", "PwrButton": "Enabled", "AcPwrRcvry": "Last", "AcPwrRcvryDelay": "Immediate", "UefiVariableAccess": "Standard", "SecureBoot": "Disabled", "SecureBootPolicy": "Standard", "SecureBootMode": "DeployedMode", "AuthorizeDeviceFirmware": "Disabled", "TpmPpiBypassProvision": "Disabled", "TpmPpiBypassClear": "Disabled", "Tpm2Algorithm": "SHA1", "RedundantOsLocation": "None", "RedundantOsState": "Visible", "RedundantOsBoot": "Disabled", "MemTest": "Disabled", "DramRefreshDelay": "Minimum", "MemOpMode": "OptimizerMode", "MemoryInterleaving": "Auto", "CorrEccSmi": "Enabled", "OppSrefEn": "Disabled", "CECriticalSEL": "Enabled", "DimmSlot00": "Enabled", "DimmSlot01": "Enabled", "DimmSlot02": "Enabled", "DimmSlot03": "Enabled", "DimmSlot04": "Enabled", "DimmSlot05": "Enabled", "DimmSlot06": "Enabled", "DimmSlot07": "Enabled", "DimmSlot08": "Enabled", "DimmSlot09": "Enabled", "DimmSlot10": "Enabled", "DimmSlot11": "Enabled", "DimmSlot12": "Enabled", "DimmSlot13": "Enabled", "DimmSlot14": "Enabled", "DimmSlot15": "Enabled", "NumLock": "On", "ErrPrompt": "Enabled", "ForceInt10": "Disabled", "DellWyseP25BIOSAccess": "Enabled", "PowerCycleRequest": "None", "SysPassword": null, "SetupPassword": null }, "Actions": { "#Bios.ChangePassword": { "target": "/redfish/v1/Systems/System.Embedded.1/Bios/Actions/Bios.ChangePassword" }, "#Bios.ResetBios": { "target": "/redfish/v1/Systems/System.Embedded.1/Bios/Actions/Bios.ResetBios" }, "Oem": { "#DellBios.RunBIOSLiveScanning": { "target": "/redfish/v1/Systems/System.Embedded.1/Bios/Actions/Oem/DellBios.RunBIOSLiveScanning" } } }, "@Redfish.Settings": { "@odata.context": "/redfish/v1/$metadata#Settings.Settings", "@odata.type": "#Settings.v1_3_0.Settings", "SettingsObject": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Bios/Settings" }, "SupportedApplyTimes": [ "OnReset", "AtMaintenanceWindowStart", "InMaintenanceWindowOnReset" ] }, "Links": { "SoftwareImages": [ { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Previous-159-2.3.6__BIOS.Setup.1-1" }, { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Installed-159-2.2.4__BIOS.Setup.1-1" }, { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Current-159-2.2.4__BIOS.Setup.1-1" } ], "SoftwareImages@odata.count": 3, "ActiveSoftwareImage": { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Installed-159-2.2.4__BIOS.Setup.1-1" } } } ================================================ FILE: internal/redfishwrapper/fixtures/dell/manager.idrac.embedded.1.json ================================================ { "@odata.context": "/redfish/v1/$metadata#Manager.Manager", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1", "@odata.type": "#Manager.v1_20_0.Manager", "Id": "iDRAC.Embedded.1", "Name": "Manager", "Description": "BMC", "ManagerType": "BMC", "FirmwareVersion": "1.20.50.52", "Model": "17G Monolithic", "PowerState": "On", "Status": { "State": "Enabled", "Health": "OK" }, "GraphicalConsole": { "ServiceEnabled": true, "MaxConcurrentSessions": 6, "ConnectTypesSupported": [ "KVMIP" ] }, "CommandShell": { "ServiceEnabled": true, "MaxConcurrentSessions": 5, "ConnectTypesSupported": [ "SSH", "IPMI" ] }, "LogServices": { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices" }, "EthernetInterfaces": { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/EthernetInterfaces" }, "NetworkProtocol": { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/NetworkProtocol" }, "Links": { "ManagerForServers@odata.count": 1, "ManagerForServers": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1" } ], "ManagerForChassis@odata.count": 1, "ManagerForChassis": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1" } ], "ManagerInChassis": { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1" } }, "Actions": { "#Manager.Reset": { "target": "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Manager.Reset", "ResetType@Redfish.AllowableValues": [ "GracefulRestart" ] } } } ================================================ FILE: internal/redfishwrapper/fixtures/dell/managers.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ManagerCollection.ManagerCollection", "@odata.id": "/redfish/v1/Managers", "@odata.type": "#ManagerCollection.ManagerCollection", "Description": "BMC Manager Collection", "Members": [ { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1" } ], "Members@odata.count": 1, "Name": "Manager Collection" } ================================================ FILE: internal/redfishwrapper/fixtures/dell/serviceroot.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ServiceRoot.ServiceRoot", "@odata.id": "/redfish/v1", "@odata.type": "#ServiceRoot.v1_6_0.ServiceRoot", "AccountService": { "@odata.id": "/redfish/v1/AccountService" }, "CertificateService": { "@odata.id": "/redfish/v1/CertificateService" }, "Chassis": { "@odata.id": "/redfish/v1/Chassis" }, "Description": "Root Service", "EventService": { "@odata.id": "/redfish/v1/EventService" }, "Fabrics": { "@odata.id": "/redfish/v1/Fabrics" }, "Id": "RootService", "JobService": { "@odata.id": "/redfish/v1/JobService" }, "JsonSchemas": { "@odata.id": "/redfish/v1/JsonSchemas" }, "Links": { "Sessions": { "@odata.id": "/redfish/v1/SessionService/Sessions" } }, "Managers": { "@odata.id": "/redfish/v1/Managers" }, "Name": "Root Service", "Oem": { "Dell": { "@odata.context": "/redfish/v1/$metadata#DellServiceRoot.DellServiceRoot", "@odata.type": "#DellServiceRoot.v1_0_0.DellServiceRoot", "IsBranded": 0, "ManagerMACAddress": "d0:8e:79:bb:3e:ea", "ServiceTag": "FOOBAR" } }, "Product": "Integrated Dell Remote Access Controller", "ProtocolFeaturesSupported": { "ExcerptQuery": false, "ExpandQuery": { "ExpandAll": true, "Levels": true, "Links": true, "MaxLevels": 1, "NoLinks": true }, "FilterQuery": true, "OnlyMemberQuery": true, "SelectQuery": true }, "RedfishVersion": "1.9.0", "Registries": { "@odata.id": "/redfish/v1/Registries" }, "SessionService": { "@odata.id": "/redfish/v1/SessionService" }, "Systems": { "@odata.id": "/redfish/v1/Systems" }, "Tasks": { "@odata.id": "/redfish/v1/TaskService" }, "TelemetryService": { "@odata.id": "/redfish/v1/TelemetryService" }, "UpdateService": { "@odata.id": "/redfish/v1/UpdateService" }, "Vendor": "Dell" } ================================================ FILE: internal/redfishwrapper/fixtures/dell/system.embedded.1.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ComputerSystem.ComputerSystem", "@odata.id": "/redfish/v1/Systems/System.Embedded.1", "@odata.type": "#ComputerSystem.v1_10_0.ComputerSystem", "Actions": { "#ComputerSystem.Reset": { "target": "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset", "ResetType@Redfish.AllowableValues": [ "On", "ForceOff", "ForceRestart", "GracefulRestart", "GracefulShutdown", "PushPowerButton", "Nmi", "PowerCycle" ] } }, "AssetTag": "", "Bios": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Bios" }, "BiosVersion": "2.2.4", "Boot": { "BootOptions": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/BootOptions" }, "Certificates": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Boot/Certificates" }, "BootOrder": [ "NIC.Slot.3-1-1", "HardDisk.List.1-1" ], "BootOrder@odata.count": 2, "BootSourceOverrideEnabled": "Disabled", "BootSourceOverrideMode": "Legacy", "BootSourceOverrideTarget": "None", "UefiTargetBootSourceOverride": null, "BootSourceOverrideTarget@Redfish.AllowableValues": [ "None", "Pxe", "Floppy", "Cd", "Hdd", "BiosSetup", "Utilities", "UefiTarget", "SDCard", "UefiHttp" ] }, "Description": "Computer System which represents a machine (physical or virtual) and the local resources such as memory, cpu and other devices that can be accessed from that machine.", "EthernetInterfaces": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/EthernetInterfaces" }, "HostName": "", "HostWatchdogTimer": { "FunctionEnabled": false, "Status": { "State": "Disabled" }, "TimeoutAction": "None" }, "HostingRoles": [], "HostingRoles@odata.count": 0, "Id": "System.Embedded.1", "IndicatorLED": "Lit", "Links": { "Chassis": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1" } ], "Chassis@odata.count": 1, "CooledBy": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/0" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/1" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/2" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/3" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/4" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/5" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/6" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/7" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/8" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/9" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/10" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/11" } ], "CooledBy@odata.count": 12, "ManagedBy": [ { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1" } ], "ManagedBy@odata.count": 1, "Oem": { "Dell": { "@odata.type": "#DellOem.v1_1_0.DellOemLinks", "BootOrder": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBootSources" }, "DellBootSources": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBootSources" }, "DellSoftwareInstallationService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSoftwareInstallationService" }, "DellVideoCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellVideo" }, "DellChassisCollection": { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Oem/Dell/DellChassis" }, "DellPresenceAndStatusSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellPresenceAndStatusSensors" }, "DellSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSensors" }, "DellRollupStatusCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellRollupStatus" }, "DellPSNumericSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellPSNumericSensors" }, "DellVideoNetworkCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellVideoNetwork" }, "DellOSDeploymentService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellOSDeploymentService" }, "DellMetricService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellMetricService" }, "DellGPUSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellGPUSensors" }, "DellRaidService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellRaidService" }, "DellNumericSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellNumericSensors" }, "DellBIOSService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBIOSService" }, "DellSlotCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSlots" } } }, "PoweredBy": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power#/PowerSupplies/0" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power#/PowerSupplies/1" } ], "PoweredBy@odata.count": 2 }, "Manufacturer": "Dell Inc.", "Memory": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Memory" }, "MemorySummary": { "MemoryMirroring": "System", "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" }, "TotalSystemMemoryGiB": 64 }, "Model": "PowerEdge R6515", "Name": "System", "NetworkInterfaces": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/NetworkInterfaces" }, "Oem": { "Dell": { "@odata.type": "#DellOem.v1_1_0.DellOemResources", "DellSystem": { "BIOSReleaseDate": "04/12/2021", "BaseBoardChassisSlot": "NA", "BatteryRollupStatus": "OK", "BladeGeometry": "NotApplicable", "CMCIP": null, "CPURollupStatus": "OK", "ChassisModel": "", "ChassisName": "Main System Chassis", "ChassisServiceTag": "FOOXD53", "ChassisSystemHeightUnit": 1, "CurrentRollupStatus": "OK", "EstimatedExhaustTemperatureCelsius": 255, "EstimatedSystemAirflowCFM": 255, "ExpressServiceCode": "33944916423", "FanRollupStatus": "OK", "Id": "System.Embedded.1", "IDSDMRollupStatus": null, "IntrusionRollupStatus": "OK", "IsOEMBranded": "False", "LastSystemInventoryTime": "2022-04-01T17:01:30+00:00", "LastUpdateTime": "2022-01-14T15:14:30+00:00", "LicensingRollupStatus": "OK", "MaxCPUSockets": 1, "MaxDIMMSlots": 16, "MaxPCIeSlots": 5, "MemoryOperationMode": "OptimizerMode", "Name": "DellSystem", "NodeID": "FOOXD53", "PSRollupStatus": "OK", "PlatformGUID": "3335444f-c0c6-5880-4410-004c4c4c4544", "PopulatedDIMMSlots": 8, "PopulatedPCIeSlots": 2, "PowerCapEnabledState": "Disabled", "SDCardRollupStatus": null, "SELRollupStatus": "OK", "ServerAllocationWatts": null, "StorageRollupStatus": "OK", "SysMemErrorMethodology": "Multi-bitECC", "SysMemFailOverState": "NotInUse", "SysMemLocation": "SystemBoardOrMotherboard", "SysMemPrimaryStatus": "OK", "SystemGeneration": "15G Monolithic", "SystemID": 2300, "SystemRevision": "I", "TempRollupStatus": "OK", "TempStatisticsRollupStatus": "OK", "UUID": "4c4c4544-004c-4410-8058-c6c04f443533", "VoltRollupStatus": "OK", "smbiosGUID": "44454c4c-4c00-1044-8058-c6c04f443533", "@odata.context": "/redfish/v1/$metadata#DellSystem.DellSystem", "@odata.type": "#DellSystem.v1_2_0.DellSystem", "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSystem/System.Embedded.1" } } }, "PCIeDevices": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/195-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/72-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/194-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-8" } ], "PCIeDevices@odata.count": 32, "PCIeFunctions": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/195-0/PCIeFunctions/195-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/72-0/PCIeFunctions/72-0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-2/PCIeFunctions/128-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-3/PCIeFunctions/128-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-7/PCIeFunctions/128-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-1/PCIeFunctions/128-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-4/PCIeFunctions/128-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-8/PCIeFunctions/128-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0/PCIeFunctions/65-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0/PCIeFunctions/65-0-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/194-0/PCIeFunctions/194-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/4-0/PCIeFunctions/4-0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-1/PCIeFunctions/192-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-2/PCIeFunctions/192-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-4/PCIeFunctions/192-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-0/PCIeFunctions/192-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-3/PCIeFunctions/192-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-8/PCIeFunctions/192-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-7/PCIeFunctions/192-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-2/PCIeFunctions/64-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-3/PCIeFunctions/64-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-7/PCIeFunctions/64-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-1/PCIeFunctions/64-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-4/PCIeFunctions/64-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-8/PCIeFunctions/64-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/1-0/PCIeFunctions/1-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-2/PCIeFunctions/0-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-3/PCIeFunctions/0-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20/PCIeFunctions/0-20-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-7/PCIeFunctions/0-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20/PCIeFunctions/0-20-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-1/PCIeFunctions/0-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-4/PCIeFunctions/0-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-8/PCIeFunctions/0-8-0" } ], "PCIeFunctions@odata.count": 34, "PartNumber": "FOOCNNA00", "PowerState": "On", "ProcessorSummary": { "Count": 1, "LogicalProcessorCount": 48, "Model": "AMD EPYC 7402P 24-Core Processor", "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" } }, "Processors": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Processors" }, "SKU": "FOOXD53", "SecureBoot": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/SecureBoot" }, "SerialNumber": "CNCMU0005400IJ", "SimpleStorage": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/SimpleStorage" }, "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" }, "Storage": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage" }, "SystemType": "Physical", "TrustedModules": [ { "FirmwareVersion": "1.3.1.0", "InterfaceType": "TPM2_0", "Status": { "State": "Enabled" } } ], "TrustedModules@odata.count": 1, "UUID": "4c4c4544-004c-4410-8058-c6c04f443533" } ================================================ FILE: internal/redfishwrapper/fixtures/dell/system.embedded.1.virtualmedia.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ComputerSystem.ComputerSystem", "@odata.id": "/redfish/v1/Systems/System.Embedded.1", "@odata.type": "#ComputerSystem.v1_22_0.ComputerSystem", "Id": "System.Embedded.1", "Name": "System", "Description": "Computer System which represents a machine (physical or virtual) and the local resources such as memory, cpu and other devices that can be accessed from that machine.", "Manufacturer": "Dell Inc.", "Model": "PowerEdge R660", "PowerState": "Off", "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" }, "Boot": { "BootSourceOverrideEnabled": "Disabled", "BootSourceOverrideMode": "Legacy", "BootSourceOverrideTarget": "None" }, "VirtualMedia": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia" }, "Links": { "ManagedBy": [ { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1" } ], "ManagedBy@odata.count": 1, "Chassis": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1" } ], "Chassis@odata.count": 1 } } ================================================ FILE: internal/redfishwrapper/fixtures/dell/systems.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ComputerSystemCollection.ComputerSystemCollection", "@odata.id": "/redfish/v1/Systems", "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", "Description": "Collection of Computer Systems", "Members": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1" } ], "Members@odata.count": 1, "Name": "Computer System Collection" } ================================================ FILE: internal/redfishwrapper/fixtures/dell/virtualmedia_1.json ================================================ { "@odata.context": "/redfish/v1/$metadata#VirtualMedia.VirtualMedia", "@odata.id": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/1", "@odata.type": "#VirtualMedia.v1_6_4.VirtualMedia", "@odata.etag": "W/\"gen-2\"", "Id": "1", "Name": "VirtualMedia Instance 1", "Description": "iDRAC Virtual Media Instance", "Image": null, "ImageName": null, "Inserted": false, "ConnectedVia": "NotConnected", "WriteProtected": null, "TransferMethod": null, "TransferProtocolType": null, "VerifyCertificate": false, "MediaTypes": [ "CD", "DVD", "USBStick" ], "MediaTypes@odata.count": 3, "Actions": { "#VirtualMedia.InsertMedia": { "target": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/1/Actions/VirtualMedia.InsertMedia", "TransferProtocolType@Redfish.AllowableValues": [ "CIFS", "HTTP", "HTTPS", "NFS" ], "TransferMethod@Redfish.AllowableValues": [ "Stream" ] }, "#VirtualMedia.EjectMedia": { "target": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/1/Actions/VirtualMedia.EjectMedia" } }, "Certificates": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/1/Certificates" } } ================================================ FILE: internal/redfishwrapper/fixtures/dell/virtualmedia_2.json ================================================ { "@odata.context": "/redfish/v1/$metadata#VirtualMedia.VirtualMedia", "@odata.id": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/2", "@odata.type": "#VirtualMedia.v1_6_4.VirtualMedia", "@odata.etag": "W/\"gen-2\"", "Id": "2", "Name": "VirtualMedia Instance 2", "Description": "iDRAC Virtual Media Instance", "Image": null, "ImageName": null, "Inserted": false, "ConnectedVia": "NotConnected", "WriteProtected": null, "TransferMethod": null, "TransferProtocolType": null, "VerifyCertificate": false, "MediaTypes": [ "CD", "DVD", "USBStick" ], "MediaTypes@odata.count": 3, "Actions": { "#VirtualMedia.InsertMedia": { "target": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/2/Actions/VirtualMedia.InsertMedia", "TransferProtocolType@Redfish.AllowableValues": [ "CIFS", "HTTP", "HTTPS", "NFS" ], "TransferMethod@Redfish.AllowableValues": [ "Stream" ] }, "#VirtualMedia.EjectMedia": { "target": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/2/Actions/VirtualMedia.EjectMedia" } }, "Certificates": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/2/Certificates" } } ================================================ FILE: internal/redfishwrapper/fixtures/dell/virtualmedia_collection.json ================================================ { "@odata.context": "/redfish/v1/$metadata#VirtualMediaCollection.VirtualMediaCollection", "@odata.id": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia", "@odata.type": "#VirtualMediaCollection.VirtualMediaCollection", "@odata.etag": "W/\"gen-1\"", "Description": "A collection of VirtualMediaCollection", "Members": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/1" } ], "Members@odata.count": 2, "Name": "Virtual Media Services" } ================================================ FILE: internal/redfishwrapper/fixtures/managers.json ================================================ { "@odata.type": "#ManagerCollection.ManagerCollection", "@odata.id": "/redfish/v1/Managers", "Name": "Manager Collection", "Description": "Manager Collection", "Members@odata.count": 1, "Members": [ { "@odata.id": "/redfish/v1/Managers/1" } ] } ================================================ FILE: internal/redfishwrapper/fixtures/managers_1.json ================================================ { "@odata.type": "#Manager.v1_7_0.Manager", "@odata.id": "/redfish/v1/Managers/1", "Id": "1", "Name": "Manager", "Description": "BMC", "ManagerType": "BMC", "UUID": "00000000-0000-0000-0000-3CECEFCEFEDA", "Model": "ASPEED", "FirmwareVersion": "01.13.04", "DateTime": "2023-11-06T14:16:52Z", "DateTimeLocalOffset": "+00:00", "Status": { "State": "Enabled", "Health": "OK" }, "GraphicalConsole": { "ServiceEnabled": true, "MaxConcurrentSessions": 4, "ConnectTypesSupported": [ "KVMIP" ] }, "SerialConsole": { "ServiceEnabled": true, "MaxConcurrentSessions": 1, "ConnectTypesSupported": [ "SSH", "IPMI" ] }, "CommandShell": { "ServiceEnabled": true, "MaxConcurrentSessions": 0, "ConnectTypesSupported": [ "SSH" ] }, "NetworkProtocol": { "@odata.id": "/redfish/v1/Managers/1/NetworkProtocol" }, "EthernetInterfaces": { "@odata.id": "/redfish/v1/Managers/1/EthernetInterfaces" }, "SerialInterfaces": { "@odata.id": "/redfish/v1/Managers/1/SerialInterfaces" }, "LogServices": { "@odata.id": "/redfish/v1/Managers/1/LogServices" }, "VirtualMedia": { "@odata.id": "/redfish/v1/Managers/1/VirtualMedia" }, "HostInterfaces": { "@odata.id": "/redfish/v1/Managers/1/HostInterfaces" }, "LldpService": { "@odata.id": "/redfish/v1/Managers/1/LldpService" }, "Links": { "ManagerForServers@odata.count": 1, "ManagerForServers": [ { "@odata.id": "/redfish/v1/Systems/1" } ], "ManagerForChassis@odata.count": 1, "ManagerForChassis": [ { "@odata.id": "/redfish/v1/Chassis/1" } ], "ManagerInChassis": { "@odata.id": "/redfish/v1/Chassis/1/" }, "ActiveSoftwareImage": { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BMC" }, "SoftwareImages@odata.count": 1, "SoftwareImages": [ { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BMC" } ], "Oem": {} }, "Actions": { "#Manager.Reset": { "target": "/redfish/v1/Managers/1/Actions/Manager.Reset" } } } ================================================ FILE: internal/redfishwrapper/fixtures/serviceroot.json ================================================ { "@odata.type": "#ServiceRoot.v1_5_2.ServiceRoot", "@odata.id": "/redfish/v1", "Id": "ServiceRoot", "Name": "Root Service", "RedfishVersion": "1.9.0", "UUID": "00000000-0000-0000-0000-3CECEFCEFEDA", "Systems": { "@odata.id": "/redfish/v1/Systems" }, "Chassis": { "@odata.id": "/redfish/v1/Chassis" }, "Managers": { "@odata.id": "/redfish/v1/Managers" }, "Tasks": { "@odata.id": "/redfish/v1/TaskService" }, "SessionService": { "@odata.id": "/redfish/v1/SessionService" }, "AccountService": { "@odata.id": "/redfish/v1/AccountService" }, "EventService": { "@odata.id": "/redfish/v1/EventService" }, "UpdateService": { "@odata.id": "/redfish/v1/UpdateService" }, "CertificateService": { "@odata.id": "/redfish/v1/CertificateService" }, "Registries": { "@odata.id": "/redfish/v1/Registries" }, "JsonSchemas": { "@odata.id": "/redfish/v1/JsonSchemas" }, "TelemetryService": { "@odata.id": "/redfish/v1/TelemetryService" }, "Links": { "Sessions": { "@odata.id": "/redfish/v1/SessionService/Sessions" } }, "ProtocolFeaturesSupported": { "FilterQuery": true, "SelectQuery": true, "ExcerptQuery": false, "OnlyMemberQuery": false, "ExpandQuery": { "Links": true, "NoLinks": true, "ExpandAll": true, "Levels": true, "MaxLevels": 2 } } } ================================================ FILE: internal/redfishwrapper/fixtures/serviceroot_no_manager.json ================================================ { "@odata.type": "#ServiceRoot.v1_5_2.ServiceRoot", "@odata.id": "/redfish/v1", "Id": "ServiceRoot", "Name": "Root Service", "RedfishVersion": "1.9.0", "UUID": "00000000-0000-0000-0000-3CECEFCEFEDA", "Systems": { "@odata.id": "/redfish/v1/Systems" }, "Chassis": { "@odata.id": "/redfish/v1/Chassis" }, "Tasks": { "@odata.id": "/redfish/v1/TaskService" }, "SessionService": { "@odata.id": "/redfish/v1/SessionService" }, "AccountService": { "@odata.id": "/redfish/v1/AccountService" }, "EventService": { "@odata.id": "/redfish/v1/EventService" }, "UpdateService": { "@odata.id": "/redfish/v1/UpdateService" }, "CertificateService": { "@odata.id": "/redfish/v1/CertificateService" }, "Registries": { "@odata.id": "/redfish/v1/Registries" }, "JsonSchemas": { "@odata.id": "/redfish/v1/JsonSchemas" }, "TelemetryService": { "@odata.id": "/redfish/v1/TelemetryService" }, "Links": { "Sessions": { "@odata.id": "/redfish/v1/SessionService/Sessions" } }, "ProtocolFeaturesSupported": { "FilterQuery": true, "SelectQuery": true, "ExcerptQuery": false, "OnlyMemberQuery": false, "ExpandQuery": { "Links": true, "NoLinks": true, "ExpandAll": true, "Levels": true, "MaxLevels": 2 } } } ================================================ FILE: internal/redfishwrapper/fixtures/smc_1.14.0_serviceroot.json ================================================ {"@odata.type":"#ServiceRoot.v1_14_0.ServiceRoot","@odata.id":"/redfish/v1","Id":"ServiceRoot","Name":"Root Service","RedfishVersion":"1.14.0","UUID":"00000000-0000-0000-0000-3CECEFC84895","Vendor":"Supermicro","Systems":{"@odata.id":"/redfish/v1/Systems"},"Chassis":{"@odata.id":"/redfish/v1/Chassis"},"Managers":{"@odata.id":"/redfish/v1/Managers"},"Tasks":{"@odata.id":"/redfish/v1/TaskService"},"SessionService":{"@odata.id":"/redfish/v1/SessionService"},"AccountService":{"@odata.id":"/redfish/v1/AccountService"},"EventService":{"@odata.id":"/redfish/v1/EventService"},"UpdateService":{"@odata.id":"/redfish/v1/UpdateService"},"CertificateService":{"@odata.id":"/redfish/v1/CertificateService"},"Registries":{"@odata.id":"/redfish/v1/Registries"},"JsonSchemas":{"@odata.id":"/redfish/v1/JsonSchemas"},"TelemetryService":{"@odata.id":"/redfish/v1/TelemetryService"},"Product":null,"ServiceIdentification":"S482931X2814218","Links":{"Sessions":{"@odata.id":"/redfish/v1/SessionService/Sessions"}},"Oem":{"Supermicro":{"DumpService":{"@odata.id":"/redfish/v1/Oem/Supermicro/DumpService"}}},"ProtocolFeaturesSupported":{"FilterQuery":true,"SelectQuery":true,"ExcerptQuery":false,"OnlyMemberQuery":false,"DeepOperations":{"DeepPATCH":false,"DeepPOST":false,"MaxLevels":1},"ExpandQuery":{"Links":true,"NoLinks":true,"ExpandAll":true,"Levels":true,"MaxLevels":2}},"@odata.etag":"\"a3ee7c2898ae386781519de584c4dacd\""} ================================================ FILE: internal/redfishwrapper/fixtures/smc_1.14.0_systems.json ================================================ {"@odata.type":"#ComputerSystemCollection.ComputerSystemCollection","@odata.id":"/redfish/v1/Systems","Name":"Computer System Collection","Description":"Computer System Collection","Members@odata.count":1,"Members":[{"@odata.id":"/redfish/v1/Systems/1"}],"@odata.etag":"\"e310554bb25b657853dd0b5f36f07991\""} ================================================ FILE: internal/redfishwrapper/fixtures/smc_1.14.0_systems_1.json ================================================ {"@odata.type":"#ComputerSystem.v1_16_0.ComputerSystem","@odata.id":"/redfish/v1/Systems/1","Id":"1","Name":"System","Description":"Description of server","Status":{"State":"Enabled","Health":"Critical"},"SerialNumber":"S482931X2814218","PartNumber":"SYS-510T-MR-EI018","AssetTag":null,"IndicatorLED":"Off","LocationIndicatorActive":false,"SystemType":"Physical","BiosVersion":"2.0","Manufacturer":"Supermicro","Model":"SYS-510T-MR-EI018","SKU":"To be filled by O.E.M.","UUID":"B11CC600-6D10-11EC-8000-3CECEFC846F8","ProcessorSummary":{"Count":1,"Model":"Intel(R) Xeon(R) processor","Status":{"State":"Enabled","Health":"OK","HealthRollup":"OK"},"Metrics":{"@odata.id":"/redfish/v1/Systems/1/ProcessorSummary/ProcessorMetrics"}},"MemorySummary":{"TotalSystemMemoryGiB":64,"MemoryMirroring":"System","Status":{"State":"Enabled","Health":"OK","HealthRollup":"OK"},"Metrics":{"@odata.id":"/redfish/v1/Systems/1/MemorySummary/MemoryMetrics"}},"PowerState":"On","PowerOnDelaySeconds":3,"PowerOnDelaySeconds@Redfish.AllowableNumbers":["3:254:1"],"PowerOffDelaySeconds":3,"PowerOffDelaySeconds@Redfish.AllowableNumbers":["3:254:1"],"PowerCycleDelaySeconds":5,"PowerCycleDelaySeconds@Redfish.AllowableNumbers":["5:254:1"],"Boot":{"AutomaticRetryConfig":"Disabled","BootSourceOverrideEnabled":"Continuous","BootSourceOverrideMode":"UEFI","BootSourceOverrideTarget":"Hdd","BootSourceOverrideTarget@Redfish.AllowableValues":["None","Pxe","Floppy","Cd","Usb","Hdd","BiosSetup","UsbCd","UefiBootNext","UefiHttp"],"BootOptions":{"@odata.id":"/redfish/v1/Systems/1/BootOptions"},"BootNext":null,"BootOrder":["Boot0003","Boot0004","Boot0005","Boot0006","Boot0007","Boot0008","Boot0009","Boot000A","Boot000B","Boot0002"]},"GraphicalConsole":{"ServiceEnabled":true,"Port":5900,"MaxConcurrentSessions":4,"ConnectTypesSupported":["KVMIP"]},"SerialConsole":{"MaxConcurrentSessions":1,"SSH":{"ServiceEnabled":true,"Port":22,"SharedWithManagerCLI":true,"ConsoleEntryCommand":"cd system1/sol1; start","HotKeySequenceDisplay":"press , , and then to terminate session"},"IPMI":{"HotKeySequenceDisplay":"Press ~. - terminate connection","ServiceEnabled":true,"Port":623}},"VirtualMediaConfig":{"ServiceEnabled":true,"Port":623},"BootProgress":{"OemLastState":null,"LastState":"SystemHardwareInitializationComplete"},"Processors":{"@odata.id":"/redfish/v1/Systems/1/Processors"},"Memory":{"@odata.id":"/redfish/v1/Systems/1/Memory"},"EthernetInterfaces":{"@odata.id":"/redfish/v1/Systems/1/EthernetInterfaces"},"NetworkInterfaces":{"@odata.id":"/redfish/v1/Systems/1/NetworkInterfaces"},"Storage":{"@odata.id":"/redfish/v1/Systems/1/Storage"},"LogServices":{"@odata.id":"/redfish/v1/Systems/1/LogServices"},"SecureBoot":{"@odata.id":"/redfish/v1/Systems/1/SecureBoot"},"Bios":{"@odata.id":"/redfish/v1/Systems/1/Bios"},"VirtualMedia":{"@odata.id":"/redfish/v1/Managers/1/VirtualMedia"},"Links":{"Chassis":[{"@odata.id":"/redfish/v1/Chassis/1"}],"ManagedBy":[{"@odata.id":"/redfish/v1/Managers/1"}],"PoweredBy":[{"@odata.id":"/redfish/v1/Chassis/1/PowerSubsystem/PowerSupplies/1"},{"@odata.id":"/redfish/v1/Chassis/1/PowerSubsystem/PowerSupplies/2"}]},"Actions":{"Oem":{},"#ComputerSystem.Reset":{"target":"/redfish/v1/Systems/1/Actions/ComputerSystem.Reset","@Redfish.ActionInfo":"/redfish/v1/Systems/1/ResetActionInfo","ResetType@Redfish.AllowableValues":["On","ForceOff","GracefulShutdownGracefulRestart","ForceRestart","Nmi","ForceOn"]}},"Oem":{"Supermicro":{"@odata.type":"#SmcSystemExtensions.v1_0_0.System","NodeManager":{"@odata.id":"/redfish/v1/Systems/1/Oem/Supermicro/NodeManager"}}},"@odata.etag":"\"27ffd39c216000b3013c84008394dffd\""} ================================================ FILE: internal/redfishwrapper/fixtures/smc_1.9.0_serviceroot.json ================================================ {"@odata.type":"#ServiceRoot.v1_5_2.ServiceRoot","@odata.id":"/redfish/v1","Id":"ServiceRoot","Name":"Root Service","RedfishVersion":"1.9.0","UUID":"00000000-0000-0000-0000-3CECEFC8484F","Systems":{"@odata.id":"/redfish/v1/Systems"},"Chassis":{"@odata.id":"/redfish/v1/Chassis"},"Managers":{"@odata.id":"/redfish/v1/Managers"},"Tasks":{"@odata.id":"/redfish/v1/TaskService"},"SessionService":{"@odata.id":"/redfish/v1/SessionService"},"AccountService":{"@odata.id":"/redfish/v1/AccountService"},"EventService":{"@odata.id":"/redfish/v1/EventService"},"UpdateService":{"@odata.id":"/redfish/v1/UpdateService"},"CertificateService":{"@odata.id":"/redfish/v1/CertificateService"},"Registries":{"@odata.id":"/redfish/v1/Registries"},"JsonSchemas":{"@odata.id":"/redfish/v1/JsonSchemas"},"TelemetryService":{"@odata.id":"/redfish/v1/TelemetryService"},"Links":{"Sessions":{"@odata.id":"/redfish/v1/SessionService/Sessions"}},"Oem":{"Supermicro":{"DumpService":{"@odata.id":"/redfish/v1/Oem/Supermicro/DumpService"}}},"ProtocolFeaturesSupported":{"FilterQuery":true,"SelectQuery":true,"ExcerptQuery":false,"OnlyMemberQuery":false,"ExpandQuery":{"Links":true,"NoLinks":true,"ExpandAll":true,"Levels":true,"MaxLevels":2}}} ================================================ FILE: internal/redfishwrapper/fixtures/systems.json ================================================ { "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", "@odata.id": "/redfish/v1/Systems", "Name": "Computer System Collection", "Description": "Computer System Collection", "Members@odata.count": 1, "Members": [ { "@odata.id": "/redfish/v1/Systems/1" } ] } ================================================ FILE: internal/redfishwrapper/fixtures/systems_1.json ================================================ { "@odata.type": "#ComputerSystem.v1_8_0.ComputerSystem", "@odata.id": "/redfish/v1/Systems/1", "Id": "1", "Name": "System", "Description": "Description of server", "Status": { "State": "Enabled", "Health": "Critical" }, "SerialNumber": "FOOBAR", "PartNumber": "SYS-510T-MR1-EI018", "SystemType": "Physical", "BiosVersion": "1.6", "Manufacturer": "Supermicro", "Model": "SYS-510T-MR1-EI018", "SKU": "To be filled by O.E.M.", "UUID": "0032331A-24D7-EC11-8000-3CECEFCEFEDA", "ProcessorSummary": { "Count": 1, "Model": "Intel(R) Xeon(R) processor", "Status": { "State": "Enabled", "Health": "OK", "HealthRollup": "OK" }, "Metrics": { "@odata.id": "/redfish/v1/Systems/1/ProcessorSummary/ProcessorMetrics" } }, "MemorySummary": { "TotalSystemMemoryGiB": 64, "MemoryMirroring": "System", "Status": { "State": "Enabled", "Health": "OK", "HealthRollup": "OK" }, "Metrics": { "@odata.id": "/redfish/v1/Systems/1/MemorySummary/MemoryMetrics" } }, "IndicatorLED": "Off", "PowerState": "On", "Boot": { "BootSourceOverrideEnabled": "Once", "BootSourceOverrideMode": "UEFI", "BootSourceOverrideTarget": "Hdd", "BootSourceOverrideTarget@Redfish.AllowableValues": [ "None", "Pxe", "Floppy", "Cd", "Usb", "Hdd", "BiosSetup", "UsbCd", "UefiBootNext", "UefiHttp" ], "BootOptions": { "@odata.id": "/redfish/v1/Systems/1/BootOptions" }, "BootNext": "", "BootOrder": [ "Boot0003", "Boot0006", "Boot0005" ] }, "Processors": { "@odata.id": "/redfish/v1/Systems/1/Processors" }, "Memory": { "@odata.id": "/redfish/v1/Systems/1/Memory" }, "EthernetInterfaces": { "@odata.id": "/redfish/v1/Systems/1/EthernetInterfaces" }, "NetworkInterfaces": { "@odata.id": "/redfish/v1/Systems/1/NetworkInterfaces" }, "SimpleStorage": { "@odata.id": "/redfish/v1/Systems/1/SimpleStorage" }, "Storage": { "@odata.id": "/redfish/v1/Systems/1/Storage" }, "LogServices": { "@odata.id": "/redfish/v1/Systems/1/LogServices" }, "SecureBoot": { "@odata.id": "/redfish/v1/Systems/1/SecureBoot" }, "Bios": { "@odata.id": "/redfish/v1/Systems/1/Bios" }, "Links": { "Chassis": [ { "@odata.id": "/redfish/v1/Chassis/1" } ], "ManagedBy": [ { "@odata.id": "/redfish/v1/Managers/1" } ] }, "Actions": { "#ComputerSystem.Reset": { "target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", "@Redfish.ActionInfo": "/redfish/v1/Systems/1/ResetActionInfo" } } } ================================================ FILE: internal/redfishwrapper/fixtures/systems_1_no_bios.json ================================================ { "@odata.type": "#ComputerSystem.v1_8_0.ComputerSystem", "@odata.id": "/redfish/v1/Systems/1", "Id": "1", "Name": "System", "Description": "Description of server", "Status": { "State": "Enabled", "Health": "Critical" }, "SerialNumber": "FOOBAR", "PartNumber": "SYS-510T-MR1-EI018", "SystemType": "Physical", "BiosVersion": "1.6", "Manufacturer": "Supermicro", "Model": "SYS-510T-MR1-EI018", "SKU": "To be filled by O.E.M.", "UUID": "0032331A-24D7-EC11-8000-3CECEFCEFEDA", "ProcessorSummary": { "Count": 1, "Model": "Intel(R) Xeon(R) processor", "Status": { "State": "Enabled", "Health": "OK", "HealthRollup": "OK" }, "Metrics": { "@odata.id": "/redfish/v1/Systems/1/ProcessorSummary/ProcessorMetrics" } }, "MemorySummary": { "TotalSystemMemoryGiB": 64, "MemoryMirroring": "System", "Status": { "State": "Enabled", "Health": "OK", "HealthRollup": "OK" }, "Metrics": { "@odata.id": "/redfish/v1/Systems/1/MemorySummary/MemoryMetrics" } }, "IndicatorLED": "Off", "PowerState": "On", "Boot": { "BootSourceOverrideEnabled": "Once", "BootSourceOverrideMode": "UEFI", "BootSourceOverrideTarget": "Hdd", "BootSourceOverrideTarget@Redfish.AllowableValues": [ "None", "Pxe", "Floppy", "Cd", "Usb", "Hdd", "BiosSetup", "UsbCd", "UefiBootNext", "UefiHttp" ], "BootOptions": { "@odata.id": "/redfish/v1/Systems/1/BootOptions" }, "BootNext": "", "BootOrder": [ "Boot0003", "Boot0006", "Boot0005" ] }, "Processors": { "@odata.id": "/redfish/v1/Systems/1/Processors" }, "Memory": { "@odata.id": "/redfish/v1/Systems/1/Memory" }, "EthernetInterfaces": { "@odata.id": "/redfish/v1/Systems/1/EthernetInterfaces" }, "NetworkInterfaces": { "@odata.id": "/redfish/v1/Systems/1/NetworkInterfaces" }, "SimpleStorage": { "@odata.id": "/redfish/v1/Systems/1/SimpleStorage" }, "Storage": { "@odata.id": "/redfish/v1/Systems/1/Storage" }, "LogServices": { "@odata.id": "/redfish/v1/Systems/1/LogServices" }, "SecureBoot": { "@odata.id": "/redfish/v1/Systems/1/SecureBoot" }, "Bios": { "@odata.id": "/redfish/v1/Systems/1/Bios" }, "Links": { "Chassis": [ { "@odata.id": "/redfish/v1/Chassis/1" } ], "ManagedBy": [ { "@odata.id": "/redfish/v1/Managers/1" } ] }, "Actions": { "#ComputerSystem.Reset": { "target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", "@Redfish.ActionInfo": "/redfish/v1/Systems/1/ResetActionInfo" } } } ================================================ FILE: internal/redfishwrapper/fixtures/systems_bios.json ================================================ { "@odata.context": "/redfish/v1/$metadata#Bios.Bios", "@odata.id": "/redfish/v1/Systems/1/Bios", "@odata.type": "#Bios.v1_1_1.Bios", "Id": "Bios", "Name": "BIOS Configuration Current Settings", "Description": "BIOS Configuration Current Settings", "AttributeRegistry": "BiosAttributeRegistry.v1_0_3", "Attributes": { "SmuVersion": "0.36.113.0", "DxioVersion": "36.637", "ProcCoreSpeed": "2.80 GHz", "Proc1Id": "17-31-0", "Proc1Brand": "AMD EPYC 7402P 24-Core Processor ", "Proc1L2Cache": "24x512 KB", "Proc1L3Cache": "128 MB", "Proc1Microcode": "0x8301052" } } ================================================ FILE: internal/redfishwrapper/fixtures/tasks/tasks_1_completed.json ================================================ { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/1", "Id": "1", "Name": "BIOS Verify", "TaskState": "Completed", "StartTime": "2023-11-06T12:04:16+00:00", "EndTime": "2023-11-06T12:05:31+00:00", "PercentComplete": 100, "HidePayload": true, "TaskMonitor": "/redfish/v1/TaskMonitor/fa37JncCHryDsbzayy4cBWDxS22Jjzh", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [ "" ], "Message": "", "MessageArgs": [ "" ], "Severity": "" } ], "Oem": {} } ================================================ FILE: internal/redfishwrapper/fixtures/tasks/tasks_1_failed.json ================================================ { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/1", "Id": "1", "Name": "BIOS Verify", "TaskState": "Failed", "StartTime": "2023-11-06T12:04:16+00:00", "EndTime": "2023-11-06T12:05:31+00:00", "PercentComplete": 100, "HidePayload": true, "TaskMonitor": "/redfish/v1/TaskMonitor/fa37JncCHryDsbzayy4cBWDxS22Jjzh", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [ "" ], "Message": "", "MessageArgs": [ "" ], "Severity": "" } ], "Oem": {} } ================================================ FILE: internal/redfishwrapper/fixtures/tasks/tasks_1_pending.json ================================================ { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/1", "Id": "1", "Name": "BIOS Verify", "TaskState": "Pending", "StartTime": "2023-11-06T12:04:16+00:00", "EndTime": "2023-11-06T12:05:31+00:00", "PercentComplete": 100, "HidePayload": true, "TaskMonitor": "/redfish/v1/TaskMonitor/fa37JncCHryDsbzayy4cBWDxS22Jjzh", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [ "" ], "Message": "", "MessageArgs": [ "" ], "Severity": "" } ], "Oem": {} } ================================================ FILE: internal/redfishwrapper/fixtures/tasks/tasks_1_running.json ================================================ { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/1", "Id": "1", "Name": "BIOS Verify", "TaskState": "Running", "StartTime": "2023-11-06T12:04:16+00:00", "EndTime": "2023-11-06T12:05:31+00:00", "PercentComplete": 100, "HidePayload": true, "TaskMonitor": "/redfish/v1/TaskMonitor/fa37JncCHryDsbzayy4cBWDxS22Jjzh", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [ "" ], "Message": "", "MessageArgs": [ "" ], "Severity": "" } ], "Oem": {} } ================================================ FILE: internal/redfishwrapper/fixtures/tasks/tasks_1_scheduled.json ================================================ { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/1", "Id": "1", "Name": "BIOS Verify", "TaskState": "Scheduled", "StartTime": "2023-11-06T12:04:16+00:00", "EndTime": "2023-11-06T12:05:31+00:00", "PercentComplete": 100, "HidePayload": true, "TaskMonitor": "/redfish/v1/TaskMonitor/fa37JncCHryDsbzayy4cBWDxS22Jjzh", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [ "" ], "Message": "", "MessageArgs": [ "" ], "Severity": "" } ], "Oem": {} } ================================================ FILE: internal/redfishwrapper/fixtures/tasks/tasks_1_starting.json ================================================ { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/1", "Id": "1", "Name": "BIOS Verify", "TaskState": "Starting", "StartTime": "2023-11-06T12:04:16+00:00", "EndTime": "2023-11-06T12:05:31+00:00", "PercentComplete": 100, "HidePayload": true, "TaskMonitor": "/redfish/v1/TaskMonitor/fa37JncCHryDsbzayy4cBWDxS22Jjzh", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [ "" ], "Message": "", "MessageArgs": [ "" ], "Severity": "" } ], "Oem": {} } ================================================ FILE: internal/redfishwrapper/fixtures/tasks/tasks_1_unknown.json ================================================ { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/1", "Id": "1", "Name": "BIOS Verify", "TaskState": "foobared", "StartTime": "2023-11-06T12:04:16+00:00", "EndTime": "2023-11-06T12:05:31+00:00", "PercentComplete": 100, "HidePayload": true, "TaskMonitor": "/redfish/v1/TaskMonitor/fa37JncCHryDsbzayy4cBWDxS22Jjzh", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [ "" ], "Message": "", "MessageArgs": [ "" ], "Severity": "" } ], "Oem": {} } ================================================ FILE: internal/redfishwrapper/fixtures/tasks/tasks_2.json ================================================ { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/2", "Id": "2", "Name": "BIOS Update", "TaskState": "Completed", "StartTime": "2023-11-06T12:05:47+00:00", "EndTime": "2023-11-06T12:12:37+00:00", "PercentComplete": 100, "HidePayload": true, "TaskMonitor": "/redfish/v1/TaskMonitor/MaiRrV41mtzxlYvKWrO72tK0LK0e1zL", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [ "" ], "Message": "", "MessageArgs": [ "" ], "Severity": "" } ], "Oem": {} } ================================================ FILE: internal/redfishwrapper/fixtures/tasks.json ================================================ { "@odata.type": "#TaskCollection.TaskCollection", "@odata.id": "/redfish/v1/TaskService/Tasks", "Id": "Tasks", "Name": "Task Collection", "Members@odata.count": 2, "Members": [ { "@odata.id": "/redfish/v1/TaskService/Tasks/1" }, { "@odata.id": "/redfish/v1/TaskService/Tasks/2" } ] } ================================================ FILE: internal/redfishwrapper/fixtures/taskservice.json ================================================ { "@odata.type": "#TaskService.v1_1_3.TaskService", "@odata.id": "/redfish/v1/TaskService", "Id": "TaskService", "Name": "Tasks Service", "DateTime": "2023-11-07T10:17:09Z", "CompletedTaskOverWritePolicy": "Oldest", "LifeCycleEventOnTaskStateChange": false, "Status": { "State": "Enabled", "Health": "OK" }, "ServiceEnabled": true, "Tasks": { "@odata.id": "/redfish/v1/TaskService/Tasks" }, "Oem": {} } ================================================ FILE: internal/redfishwrapper/fixtures/updateservice_disabled.json ================================================ { "@odata.context": "/redfish/v1/$metadata#UpdateService.UpdateService", "@odata.id": "/redfish/v1/UpdateService", "@odata.type": "#UpdateService.v1_8_0.UpdateService", "Actions": { "#UpdateService.SimpleUpdate": { "@Redfish.OperationApplyTimeSupport": { "@odata.type": "#Settings.v1_3_0.OperationApplyTimeSupport", "SupportedValues": [ "Immediate", "OnReset" ] }, "TransferProtocol@Redfish.AllowableValues": [ "HTTP", "NFS", "CIFS", "TFTP", "HTTPS" ], "target": "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate" } }, "Description": "Represents the properties for the Update Service", "FirmwareInventory": { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory" }, "HttpPushUri": "/redfish/v1/UpdateService/FirmwareInventory", "Id": "UpdateService", "MaxImageSizeBytes": null, "MultipartHttpPushUri": "/redfish/v1/UpdateService/MultipartUpload", "Name": "Update Service", "ServiceEnabled": false, "SoftwareInventory": { "@odata.id": "/redfish/v1/UpdateService/SoftwareInventory" }, "Status": { "Health": "OK", "State": "Enabled" } } ================================================ FILE: internal/redfishwrapper/fixtures/updateservice_ok_response.json ================================================ { "Accepted": { "code": "Base.v1_10_3.Accepted", "Message": "Successfully Accepted Request. Please see the location header and ExtendedInfo for more information.", "@Message.ExtendedInfo": [ { "MessageId": "SMC.1.0.OemSimpleupdateAcceptedMessage", "Severity": "Ok", "Resolution": "No resolution was required.", "Message": "Please also check Task Resource /redfish/v1/TaskService/Tasks/1 to see more information.", "MessageArgs": [ "/redfish/v1/TaskService/Tasks/1234" ], "RelatedProperties": [ "BiosVerifyAccepted" ] } ] } } ================================================ FILE: internal/redfishwrapper/fixtures/updateservice_unexpected_response.json ================================================ { "Accepted": { "code": "Base.v1_10_3.Accepted", "Message": "Successfully Accepted Request. Please see the location header and ExtendedInfo for more information.", "@Message.ExtendedInfo": [ { "MessageId": "SMC.1.0.OemSimpleupdateAcceptedMessage", "Severity": "Ok", "Resolution": "No resolution was required.", "RelatedProperties": [ "BiosVerifyAccepted" ] } ] } } ================================================ FILE: internal/redfishwrapper/fixtures/updateservice_with_httppushuri.json ================================================ { "@odata.id": "/redfish/v1/UpdateService", "@odata.type": "#UpdateService.v1_5_0.UpdateService", "Description": "Service for Software Update", "FirmwareInventory": { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory" }, "HttpPushUri": "/redfish/v1/UpdateService/update", "HttpPushUriOptions": { "HttpPushUriApplyTime": { "ApplyTime": "OnReset" } }, "Id": "UpdateService", "MaxImageSizeBytes": 35651584, "Name": "Update Service", "ServiceEnabled": true } ================================================ FILE: internal/redfishwrapper/fixtures/updateservice_with_multipart.json ================================================ { "@odata.context": "/redfish/v1/$metadata#UpdateService.UpdateService", "@odata.id": "/redfish/v1/UpdateService", "@odata.type": "#UpdateService.v1_8_0.UpdateService", "Actions": { "#UpdateService.SimpleUpdate": { "@Redfish.OperationApplyTimeSupport": { "@odata.type": "#Settings.v1_3_0.OperationApplyTimeSupport", "SupportedValues": [ "Immediate", "OnReset" ] }, "TransferProtocol@Redfish.AllowableValues": [ "HTTP", "NFS", "CIFS", "TFTP", "HTTPS" ], "target": "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate" } }, "Description": "Represents the properties for the Update Service", "FirmwareInventory": { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory" }, "HttpPushUri": "/redfish/v1/UpdateService/FirmwareInventory", "Id": "UpdateService", "MaxImageSizeBytes": null, "MultipartHttpPushUri": "/redfish/v1/UpdateService/MultipartUpload", "Name": "Update Service", "ServiceEnabled": true, "SoftwareInventory": { "@odata.id": "/redfish/v1/UpdateService/SoftwareInventory" }, "Status": { "Health": "OK", "State": "Enabled" } } ================================================ FILE: internal/redfishwrapper/inventory.go ================================================ package redfishwrapper import ( "context" "strings" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" "github.com/bmc-toolbox/common" "github.com/stmcginnis/gofish/schemas" ) var ( // Supported Chassis Odata IDs KnownChassisOdataIDs = []string{ // Dells "/redfish/v1/Chassis/Enclosure.Internal.0-1", "/redfish/v1/Chassis/System.Embedded.1", "/redfish/v1/Chassis/Enclosure.Internal.0-1:NonRAID.Integrated.1-1", // Supermicro "/redfish/v1/Chassis/1", // MegaRAC/ARockRack "/redfish/v1/Chassis/Self", // OpenBMC on ASRock "/redfish/v1/Chassis/ASRock_ROMED8HM3", } // Supported System Odata IDs knownSystemsOdataIDs = []string{ // Dells "/redfish/v1/Systems/System.Embedded.1", "/redfish/v1/Systems/System.Embedded.1/Bios", // Supermicros "/redfish/v1/Systems/1", // MegaRAC/ARockRack "/redfish/v1/Systems/Self", // OpenBMC on ASRock "/redfish/v1/Systems/system", } // Supported Manager Odata IDs (BMCs) managerOdataIDs = []string{ // Dells "/redfish/v1/Managers/iDRAC.Embedded.1", // Supermicros "/redfish/v1/Managers/1", // MegaRAC/ARockRack "/redfish/v1/Managers/Self", // OpenBMC on ASRock "/redfish/v1/Managers/bmc", } ) // TODO: consider removing this func (c *Client) compatibleOdataID(OdataID string, knownOdataIDs []string) bool { for _, url := range knownOdataIDs { if url == OdataID { return true } } return false } func (c *Client) Inventory(ctx context.Context, failOnError bool) (device *common.Device, err error) { updateService, err := c.UpdateService() if err != nil && failOnError { return nil, errors.Wrap(bmclibErrs.ErrRedfishSoftwareInventory, err.Error()) } softwareInventory := []*schemas.SoftwareInventory{} if updateService != nil { // nolint softwareInventory, err = updateService.FirmwareInventory() if err != nil && failOnError { return nil, errors.Wrap(bmclibErrs.ErrRedfishSoftwareInventory, err.Error()) } } // initialize device to be populated with inventory newDevice := common.NewDevice() device = &newDevice // populate device Chassis components attributes err = c.chassisAttributes(ctx, device, failOnError, softwareInventory) if err != nil && failOnError { return nil, err } // populate device System components attributes err = c.systemAttributes(device, failOnError, softwareInventory) if err != nil && failOnError { return nil, err } // populate device BMC component attributes err = c.bmcAttributes(ctx, device, softwareInventory) if err != nil && failOnError { return nil, err } return device, nil } // DeviceVendorModel returns the device vendor and model attributes // bmcAttributes collects BMC component attributes func (c *Client) bmcAttributes(ctx context.Context, device *common.Device, softwareInventory []*schemas.SoftwareInventory) (err error) { managers, err := c.Managers(ctx) if err != nil { return err } var compatible int for _, manager := range managers { if managers == nil { continue } if !c.compatibleOdataID(manager.ODataID, managerOdataIDs) { continue } compatible++ if manager.ManagerType != "BMC" { continue } device.BMC = &common.BMC{ Common: common.Common{ Description: manager.Description, Vendor: device.Vendor, Model: device.Model, Status: &common.Status{ Health: string(manager.Status.Health), State: string(manager.Status.State), }, Firmware: &common.Firmware{ Installed: manager.FirmwareVersion, }, }, ID: manager.ID, } // include additional firmware attributes from redfish firmware inventory c.firmwareAttributes("", device.BMC.ID, device.BMC.Firmware, softwareInventory) } if compatible == 0 { return bmclibErrs.ErrRedfishManagerOdataID } return nil } // chassisAttributes populates the device chassis attributes func (c *Client) chassisAttributes(ctx context.Context, device *common.Device, failOnError bool, softwareInventory []*schemas.SoftwareInventory) (err error) { chassis, err := c.Chassis(ctx) if err != nil { return err } compatible := 0 for _, ch := range chassis { if !c.compatibleOdataID(ch.ODataID, KnownChassisOdataIDs) { continue } compatible++ err = c.collectEnclosure(ch, device, softwareInventory) if err != nil && failOnError { return err } err = c.collectPSUs(ch, device, softwareInventory) if err != nil && failOnError { return err } } err = c.collectCPLDs(device, softwareInventory) if err != nil && failOnError { return err } if compatible == 0 { return bmclibErrs.ErrRedfishChassisOdataID } return nil } func (c *Client) systemAttributes(device *common.Device, failOnError bool, softwareInventory []*schemas.SoftwareInventory) (err error) { sys, err := c.System() if err != nil { return err } if !c.compatibleOdataID(sys.ODataID, knownSystemsOdataIDs) { return bmclibErrs.ErrRedfishSystemOdataID } if sys.Manufacturer != "" && sys.Model != "" && sys.SerialNumber != "" { device.Vendor = sys.Manufacturer device.Model = sys.Model device.Serial = sys.SerialNumber } type collectorFuncs []func( sys *schemas.ComputerSystem, device *common.Device, softwareInventory []*schemas.SoftwareInventory, ) error // slice of collector methods funcs := collectorFuncs{ c.collectCPUs, c.collectDIMMs, c.collectDrives, c.collectBIOS, c.collectNICs, c.collectTPMs, c.collectStorageControllers, } // execute collector methods for _, f := range funcs { err := f(sys, device, softwareInventory) if err != nil && failOnError { return err } } return nil } // firmwareInventory looks up the redfish inventory for objects that // match - 1. slug, 2. id // and returns the intalled or previous firmware for objects that matched // // slug - the component slug constant // id - the component ID // previous - when true returns previously installed firmware, else returns the current func (c *Client) firmwareAttributes(slug, id string, firmwareObj *common.Firmware, softwareInventory []*schemas.SoftwareInventory) { if len(softwareInventory) == 0 { return } if id == "" { id = slug } for _, inv := range softwareInventory { // include previously installed firmware attributes if strings.HasPrefix(inv.ID, "Previous") { if strings.Contains(inv.ID, id) || strings.EqualFold(slug, inv.Name) { if firmwareObj == nil { firmwareObj = &common.Firmware{} } if firmwareObj.Installed == inv.Version { continue } firmwareObj.Previous = append(firmwareObj.Previous, &common.Firmware{ Installed: inv.Version, SoftwareID: inv.SoftwareID, }) } } // update firmwareObj with installed firmware attributes if strings.HasPrefix(inv.ID, "Installed") { if strings.Contains(inv.ID, id) || strings.EqualFold(slug, inv.Name) { if firmwareObj == nil { firmwareObj = &common.Firmware{} } if firmwareObj.Installed == "" || firmwareObj.Installed != inv.Version { firmwareObj.Installed = inv.Version } firmwareObj.Metadata = map[string]string{"name": inv.Name} firmwareObj.SoftwareID = inv.SoftwareID } } } } ================================================ FILE: internal/redfishwrapper/inventory_collect.go ================================================ package redfishwrapper import ( "math" "strings" "github.com/bmc-toolbox/common" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/schemas" ) // defines various inventory collection helper methods // collectEnclosure collects Enclosure information func (c *Client) collectEnclosure(ch *schemas.Chassis, device *common.Device, softwareInventory []*schemas.SoftwareInventory) (err error) { e := &common.Enclosure{ Common: common.Common{ Description: ch.Description, Vendor: common.FormatVendorName(ch.Manufacturer), Model: ch.Model, Serial: ch.SerialNumber, Status: &common.Status{ Health: string(ch.Status.Health), State: string(ch.Status.State), }, Firmware: &common.Firmware{}, }, ID: ch.ID, ChassisType: string(ch.ChassisType), } if e.Model == "" && ch.PartNumber != "" { e.Model = ch.PartNumber } // include additional firmware attributes from redfish firmware inventory c.firmwareAttributes(common.SlugEnclosure, e.ID, e.Firmware, softwareInventory) device.Enclosures = append(device.Enclosures, e) return nil } // collectPSUs collects Power Supply Unit component information func (c *Client) collectPSUs(ch *schemas.Chassis, device *common.Device, softwareInventory []*schemas.SoftwareInventory) (err error) { power, err := ch.Power() if err != nil { return err } if power == nil { return nil } for _, psu := range power.PowerSupplies { p := &common.PSU{ Common: common.Common{ Description: psu.Name, Vendor: common.FormatVendorName(psu.Manufacturer), Model: psu.Model, Serial: psu.SerialNumber, Status: &common.Status{ Health: string(psu.Status.Health), State: string(psu.Status.State), }, Firmware: &common.Firmware{ Installed: psu.FirmwareVersion, }, }, ID: psu.ID, PowerCapacityWatts: int64(gofish.Deref(psu.PowerCapacityWatts)), } // include additional firmware attributes from redfish firmware inventory c.firmwareAttributes(common.SlugPSU, psu.ID, p.Firmware, softwareInventory) device.PSUs = append(device.PSUs, p) } return nil } // collectTPMs collects Trusted Platform Module component information func (c *Client) collectTPMs(sys *schemas.ComputerSystem, device *common.Device, softwareInventory []*schemas.SoftwareInventory) (err error) { for _, module := range sys.TrustedModules { //nolint:staticcheck tpm := &common.TPM{ Common: common.Common{ Firmware: &common.Firmware{ Installed: module.FirmwareVersion, }, Status: &common.Status{ State: string(module.Status.State), Health: string(module.Status.Health), }, }, InterfaceType: string(module.InterfaceType), } // include additional firmware attributes from redfish firmware inventory c.firmwareAttributes(common.SlugTPM, "TPM", tpm.Firmware, softwareInventory) device.TPMs = append(device.TPMs, tpm) } return nil } // collectNICs collects network interface component information func (c *Client) collectNICs(sys *schemas.ComputerSystem, device *common.Device, softwareInventory []*schemas.SoftwareInventory) (err error) { if sys == nil || device == nil { return nil } // collect network interface information nics, err := sys.NetworkInterfaces() if err != nil { return err } // collect network ethernet interface information, these attributes are not available in NetworkAdapter, NetworkInterfaces ethernetInterfaces, err := sys.EthernetInterfaces() if err != nil { return err } for _, nic := range nics { // collect network interface adaptor information adapter, err := nic.NetworkAdapter() if err != nil { return err } if adapter == nil { continue } n := &common.NIC{ Common: common.Common{ Vendor: common.FormatVendorName(adapter.Manufacturer), Model: adapter.Model, Serial: adapter.SerialNumber, ProductName: adapter.PartNumber, Status: &common.Status{ State: string(nic.Status.State), Health: string(nic.Status.Health), }, }, ID: nic.ID, // "Id": "NIC.Slot.3", } ports, err := adapter.NetworkPorts() if err != nil { return err } portFirmwareVersion := getFirmwareVersionFromController(adapter.Controllers, len(ports)) for _, networkPort := range ports { // populate network ports general data nicPort := &common.NICPort{} c.collectNetworkPortInfo(nicPort, adapter, networkPort, portFirmwareVersion, softwareInventory) if networkPort.ActiveLinkTechnology == schemas.EthernetLinkNetworkTechnology { // ethernet specific data c.collectEthernetInfo(nicPort, ethernetInterfaces) } n.NICPorts = append(n.NICPorts, nicPort) } // include additional firmware attributes from redfish firmware inventory c.firmwareAttributes(common.SlugNIC, n.ID, n.Firmware, softwareInventory) if len(portFirmwareVersion) > 0 { if n.Firmware == nil { n.Firmware = &common.Firmware{} } n.Firmware.Installed = portFirmwareVersion } device.NICs = append(device.NICs, n) } return nil } func (c *Client) collectNetworkPortInfo( nicPort *common.NICPort, adapter *schemas.NetworkAdapter, networkPort *schemas.NetworkPort, firmware string, softwareInventory []*schemas.SoftwareInventory, ) { if adapter != nil { nicPort.Vendor = adapter.Manufacturer nicPort.Model = adapter.Model } if networkPort != nil { nicPort.Description = networkPort.Description nicPort.PCIVendorID = networkPort.VendorID nicPort.Status = &common.Status{ Health: string(networkPort.Status.Health), State: string(networkPort.Status.State), } nicPort.ID = networkPort.ID nicPort.PhysicalID = networkPort.PhysicalPortNumber nicPort.LinkStatus = string(networkPort.LinkStatus) nicPort.ActiveLinkTechnology = string(networkPort.ActiveLinkTechnology) if networkPort.CurrentLinkSpeedMbps != nil { nicPort.SpeedBits = int64(gofish.Deref(networkPort.CurrentLinkSpeedMbps)) * int64(math.Pow10(6)) } if len(networkPort.AssociatedNetworkAddresses) > 0 { for _, macAddress := range networkPort.AssociatedNetworkAddresses { if len(macAddress) > 0 && macAddress != "00:00:00:00:00:00" { nicPort.MacAddress = macAddress // first valid value only break } } } c.firmwareAttributes(common.SlugNIC, networkPort.ID, nicPort.Firmware, softwareInventory) } if len(firmware) > 0 { if nicPort.Firmware == nil { nicPort.Firmware = &common.Firmware{} } nicPort.Firmware.Installed = firmware } } func (c *Client) collectEthernetInfo(nicPort *common.NICPort, ethernetInterfaces []*schemas.EthernetInterface) { if nicPort == nil { return } // populate mac address et al. from matching ethernet interface for _, ethInterface := range ethernetInterfaces { // the ethernet interface includes the port, position number and function NIC.Slot.3-1-1 if !strings.HasPrefix(ethInterface.ID, nicPort.ID) { continue } // override values only if needed if len(ethInterface.Description) > 0 { nicPort.Description = ethInterface.Description } if len(ethInterface.Status.Health) > 0 { if nicPort.Status == nil { nicPort.Status = &common.Status{} } nicPort.Status.Health = string(ethInterface.Status.Health) } if len(ethInterface.Status.State) > 0 { if nicPort.Status == nil { nicPort.Status = &common.Status{} } nicPort.Status.State = string(ethInterface.Status.State) } nicPort.ID = ethInterface.ID // override ID if ethInterface.SpeedMbps != nil { nicPort.SpeedBits = int64(gofish.Deref(ethInterface.SpeedMbps)) * int64(math.Pow10(6)) } nicPort.AutoNeg = ethInterface.AutoNeg nicPort.MTUSize = gofish.Deref(ethInterface.MTUSize) // always override mac address nicPort.MacAddress = ethInterface.MACAddress break // stop at first match } } func getFirmwareVersionFromController(controllers []schemas.Controllers, portCount int) string { for _, controller := range controllers { if gofish.Deref(controller.ControllerCapabilities.NetworkPortCount) == portCount { return controller.FirmwarePackageVersion } } return "" } func (c *Client) collectBIOS(sys *schemas.ComputerSystem, device *common.Device, softwareInventory []*schemas.SoftwareInventory) (err error) { device.BIOS = &common.BIOS{ Common: common.Common{ Firmware: &common.Firmware{ Installed: sys.BiosVersion, }, }, } bios, err := sys.Bios() if err != nil { return err } if bios != nil { device.BIOS.Description = bios.Description } // include additional firmware attributes from redfish firmware inventory c.firmwareAttributes(common.SlugBIOS, "BIOS", device.BIOS.Firmware, softwareInventory) return nil } // collectDrives collects drive component information func (c *Client) collectDrives(sys *schemas.ComputerSystem, device *common.Device, softwareInventory []*schemas.SoftwareInventory) (err error) { storage, err := sys.Storage() if err != nil { return err } for _, member := range storage { if member.DrivesCount == 0 { continue } drives, err := member.Drives() if err != nil { return err } for _, drive := range drives { d := &common.Drive{ Common: common.Common{ ProductName: drive.Model, Description: drive.Description, Serial: drive.SerialNumber, Vendor: common.FormatVendorName(drive.Manufacturer), Model: drive.Model, Firmware: &common.Firmware{ Installed: drive.Revision, }, Status: &common.Status{ Health: string(drive.Status.Health), State: string(drive.Status.State), }, }, ID: drive.ID, Type: string(drive.MediaType), StorageController: member.ID, Protocol: string(drive.Protocol), CapacityBytes: int64(gofish.Deref(drive.CapacityBytes)), CapableSpeedGbps: int64(gofish.Deref(drive.CapableSpeedGbs)), NegotiatedSpeedGbps: int64(gofish.Deref(drive.NegotiatedSpeedGbs)), BlockSizeBytes: int64(gofish.Deref(drive.BlockSizeBytes)), } // include additional firmware attributes from redfish firmware inventory c.firmwareAttributes("Disk", drive.ID, d.Firmware, softwareInventory) device.Drives = append(device.Drives, d) } } return nil } // collectStorageControllers populates the device with Storage controller component attributes func (c *Client) collectStorageControllers(sys *schemas.ComputerSystem, device *common.Device, softwareInventory []*schemas.SoftwareInventory) (err error) { storage, err := sys.Storage() if err != nil { return err } for _, member := range storage { for _, controller := range member.StorageControllers { //nolint:staticcheck cs := &common.StorageController{ Common: common.Common{ Description: controller.Name, Vendor: common.FormatVendorName(controller.Manufacturer), Model: controller.PartNumber, Serial: controller.SerialNumber, Status: &common.Status{ Health: string(controller.Status.Health), State: string(controller.Status.State), }, Firmware: &common.Firmware{ Installed: controller.FirmwareVersion, }, }, ID: controller.ID, SpeedGbps: int64(gofish.Deref(controller.SpeedGbps)), } // In some cases the storage controller model number is present in the Name field if strings.TrimSpace(cs.Model) == "" && strings.TrimSpace(controller.Name) != "" { cs.Model = controller.Name } // include additional firmware attributes from redfish firmware inventory c.firmwareAttributes(cs.Description, cs.ID, cs.Firmware, softwareInventory) device.StorageControllers = append(device.StorageControllers, cs) } } return nil } // collectCPUs populates the device with CPU component attributes func (c *Client) collectCPUs(sys *schemas.ComputerSystem, device *common.Device, _ []*schemas.SoftwareInventory) (err error) { procs, err := sys.Processors() if err != nil { return err } for _, proc := range procs { if proc.ProcessorType != "CPU" { // TODO: handle this case continue } device.CPUs = append(device.CPUs, &common.CPU{ Common: common.Common{ Description: proc.Description, Vendor: common.FormatVendorName(proc.Manufacturer), Model: proc.Model, Serial: "", Status: &common.Status{ Health: string(proc.Status.Health), State: string(proc.Status.State), }, Firmware: &common.Firmware{ Installed: proc.ProcessorID.MicrocodeInfo, }, }, ID: proc.ID, Architecture: string(proc.ProcessorArchitecture), Slot: proc.Socket, ClockSpeedHz: int64(gofish.Deref(proc.MaxSpeedMHz) * 1000 * 1000), Cores: gofish.Deref(proc.TotalCores), Threads: gofish.Deref(proc.TotalThreads), }) } return nil } // collectDIMMs populates the device with memory component attributes func (c *Client) collectDIMMs(sys *schemas.ComputerSystem, device *common.Device, softwareInventory []*schemas.SoftwareInventory) (err error) { dimms, err := sys.Memory() if err != nil { return err } for _, dimm := range dimms { device.Memory = append(device.Memory, &common.Memory{ Common: common.Common{ Description: dimm.Description, Vendor: common.FormatVendorName(dimm.Manufacturer), Model: "", Serial: dimm.SerialNumber, Status: &common.Status{ Health: string(dimm.Status.Health), State: string(dimm.Status.State), }, }, Slot: dimm.ID, Type: string(dimm.MemoryType), SizeBytes: int64(gofish.Deref(dimm.VolatileSizeMiB) * 1024 * 1024), FormFactor: "", PartNumber: dimm.PartNumber, ClockSpeedHz: int64(gofish.Deref(dimm.OperatingSpeedMhz) * 1000 * 1000), }) } return nil } // collecCPLDs populates the device with CPLD component attributes func (c *Client) collectCPLDs(device *common.Device, softwareInventory []*schemas.SoftwareInventory) (err error) { cpld := &common.CPLD{ Common: common.Common{ Vendor: common.FormatVendorName(device.Vendor), Model: device.Model, Firmware: &common.Firmware{Metadata: make(map[string]string)}, }, } c.firmwareAttributes(common.SlugCPLD, "", cpld.Firmware, softwareInventory) name, exists := cpld.Firmware.Metadata["name"] if exists { cpld.Description = name } device.CPLDs = []*common.CPLD{cpld} return nil } ================================================ FILE: internal/redfishwrapper/inventory_collect_test.go ================================================ package redfishwrapper import ( "reflect" "testing" "github.com/bmc-toolbox/common" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/schemas" ) func TestInventoryCollectNetworkPortInfo(t *testing.T) { testAdapter := &schemas.NetworkAdapter{ Manufacturer: "Acme", Model: "Anvil 3000", } testNetworkPort := &schemas.NetworkPort{ Entity: schemas.Entity{ID: "NetworkPort-1"}, VendorID: "Vendor-ID", PhysicalPortNumber: "10", LinkStatus: "Up", ActiveLinkTechnology: "Ethernet", CurrentLinkSpeedMbps: gofish.ToRef(1000), AssociatedNetworkAddresses: []string{"98:E7:43:00:01:0A"}, } testFirmwareVersion := "1.2.3" wNicPortOnlyAdapter := &common.NICPort{Common: common.Common{Vendor: testAdapter.Manufacturer, Model: testAdapter.Model}} wNicPortOnlyNPort := &common.NICPort{ Common: common.Common{ Description: testNetworkPort.Description, PCIVendorID: testNetworkPort.VendorID, Status: &common.Status{ Health: string(testNetworkPort.Status.Health), State: string(testNetworkPort.Status.State), }, }, ID: testNetworkPort.ID, PhysicalID: testNetworkPort.PhysicalPortNumber, LinkStatus: string(testNetworkPort.LinkStatus), ActiveLinkTechnology: string(testNetworkPort.ActiveLinkTechnology), SpeedBits: 1000000000, MacAddress: testNetworkPort.AssociatedNetworkAddresses[0], } wNicPortOnlyFirmware := &common.NICPort{Common: common.Common{Firmware: &common.Firmware{Installed: testFirmwareVersion}}} wNicPortFull := &common.NICPort{ Common: common.Common{ Description: testNetworkPort.Description, Vendor: testAdapter.Manufacturer, Model: testAdapter.Model, PCIVendorID: testNetworkPort.VendorID, Firmware: &common.Firmware{Installed: testFirmwareVersion}, Status: &common.Status{ Health: string(testNetworkPort.Status.Health), State: string(testNetworkPort.Status.State), }, }, ID: testNetworkPort.ID, PhysicalID: testNetworkPort.PhysicalPortNumber, LinkStatus: string(testNetworkPort.LinkStatus), ActiveLinkTechnology: string(testNetworkPort.ActiveLinkTechnology), SpeedBits: 1000000000, MacAddress: testNetworkPort.AssociatedNetworkAddresses[0], } tests := []struct { name string nicPort *common.NICPort adapter *schemas.NetworkAdapter networkPort *schemas.NetworkPort firmware string wantedNicPort *common.NICPort }{ {name: "nil"}, {name: "empty", nicPort: &common.NICPort{}, wantedNicPort: &common.NICPort{}}, { name: "only adapter", nicPort: &common.NICPort{}, adapter: testAdapter, wantedNicPort: wNicPortOnlyAdapter, }, { name: "only network port", nicPort: &common.NICPort{}, networkPort: testNetworkPort, wantedNicPort: wNicPortOnlyNPort, }, { name: "only firmware", nicPort: &common.NICPort{}, firmware: testFirmwareVersion, wantedNicPort: wNicPortOnlyFirmware, }, { name: "full", nicPort: &common.NICPort{}, adapter: testAdapter, networkPort: testNetworkPort, firmware: testFirmwareVersion, wantedNicPort: wNicPortFull, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := Client{} c.collectNetworkPortInfo(tt.nicPort, tt.adapter, tt.networkPort, tt.firmware, []*schemas.SoftwareInventory{}) if !reflect.DeepEqual(tt.nicPort, tt.wantedNicPort) { t.Errorf("collectNetworkPortInfo() gotNicPort = %v, want %v", tt.nicPort, tt.wantedNicPort) } }) } } func TestInventoryCollectEthernetInfo(t *testing.T) { testNicPortID := "test NIC port ID" testEthernetID := "test NIC port ID ethernet" testNicPort := &common.NICPort{ ID: testNicPortID, } testUnmatchingEthList := []*schemas.EthernetInterface{ {Entity: schemas.Entity{ID: "other ID"}}, {Entity: schemas.Entity{ID: "another one"}}, } testMatchingEth := &schemas.EthernetInterface{ Entity: schemas.Entity{ID: testEthernetID}, Status: schemas.Status{ Health: "OK", State: "Enabled", }, SpeedMbps: gofish.ToRef(10000), AutoNeg: true, MTUSize: gofish.ToRef(1500), MACAddress: "f6:a9:26:e3:e6:32", } testMatchingEthList := append(testUnmatchingEthList, testMatchingEth) wNicPortFull := &common.NICPort{ Common: common.Common{ Description: testMatchingEth.Description, Status: &common.Status{ Health: string(testMatchingEth.Status.Health), State: string(testMatchingEth.Status.State), }, }, ID: testMatchingEth.ID, SpeedBits: 10000000000, AutoNeg: testMatchingEth.AutoNeg, MTUSize: gofish.Deref(testMatchingEth.MTUSize), MacAddress: testMatchingEth.MACAddress, } tests := []struct { name string nicPort *common.NICPort ethernetInterfaces []*schemas.EthernetInterface wantedNicPort *common.NICPort }{ {name: "nil"}, {name: "empty", nicPort: testNicPort, wantedNicPort: testNicPort}, {name: "empty ethernet list", nicPort: testNicPort, ethernetInterfaces: []*schemas.EthernetInterface{}, wantedNicPort: testNicPort}, {name: "unmatching ethernet list", nicPort: testNicPort, ethernetInterfaces: testUnmatchingEthList, wantedNicPort: testNicPort}, { name: "full", nicPort: testNicPort, ethernetInterfaces: testMatchingEthList, wantedNicPort: wNicPortFull}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := Client{} c.collectEthernetInfo(tt.nicPort, tt.ethernetInterfaces) }) } } ================================================ FILE: internal/redfishwrapper/main_test.go ================================================ package redfishwrapper import ( "io" "log" "net/http" "os" "testing" ) func mustReadFile(t *testing.T, filename string) []byte { t.Helper() fixture := fixturesDir + "/" + filename fh, err := os.Open(fixture) if err != nil { log.Fatal(err) } defer fh.Close() b, err := io.ReadAll(fh) if err != nil { log.Fatal(err) } return b } var endpointFunc = func(t *testing.T, file string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if file == "404" { w.WriteHeader(http.StatusNotFound) } // expect either GET or Delete methods if r.Method != http.MethodGet && r.Method != http.MethodDelete { w.WriteHeader(http.StatusNotFound) return } _, _ = w.Write(mustReadFile(t, file)) } } ================================================ FILE: internal/redfishwrapper/power.go ================================================ package redfishwrapper import ( "context" "fmt" "strings" "time" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" "github.com/stmcginnis/gofish/schemas" ) // PowerSet sets the power state of a server func (c *Client) PowerSet(ctx context.Context, state string) (ok bool, err error) { // TODO: create consts for the state values switch strings.ToLower(state) { case "on": return c.SystemPowerOn(ctx) case "off": return c.SystemForceOff(ctx) case "soft": return c.SystemPowerOff(ctx) case "reset": return c.SystemReset(ctx) case "cycle": return c.SystemPowerCycle(ctx) default: return false, errors.New("unknown power action") } } // BMCReset powercycles the BMC. func (c *Client) BMCReset(ctx context.Context, resetType string) (ok bool, err error) { if err := c.SessionActive(); err != nil { return false, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } manager, err := c.Manager(ctx) if err != nil { return false, err } if _, err = manager.Reset(schemas.ResetType(resetType)); err != nil { return false, err } return true, nil } // SystemPowerOn powers on the system. func (c *Client) SystemPowerOn(ctx context.Context) (ok bool, err error) { if err := c.SessionActive(); err != nil { return false, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } system, err := c.System() if err != nil { return false, err } if system.PowerState == schemas.OnPowerState { return true, nil } system.DisableEtagMatch(c.disableEtagMatch) if _, err = system.Reset(schemas.OnResetType); err != nil { return false, err } return true, nil } // SystemPowerOff powers off the system. func (c *Client) SystemPowerOff(ctx context.Context) (ok bool, err error) { if err := c.SessionActive(); err != nil { return false, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } system, err := c.System() if err != nil { return false, err } if system.PowerState == schemas.OffPowerState { return true, nil } system.DisableEtagMatch(c.disableEtagMatch) if _, err = system.Reset(schemas.GracefulShutdownResetType); err != nil { return false, err } return false, nil } // SystemReset power cycles the system. func (c *Client) SystemReset(ctx context.Context) (ok bool, err error) { if err := c.SessionActive(); err != nil { return false, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } system, err := c.System() if err != nil { return false, err } system.DisableEtagMatch(c.disableEtagMatch) if _, err = system.Reset(schemas.PowerCycleResetType); err != nil { _, _ = c.SystemPowerOff(ctx) for wait := 1; wait < 10; wait++ { status, _ := c.SystemPowerStatus(ctx) if status == "off" { break } time.Sleep(1 * time.Second) } return c.SystemPowerOn(ctx) } return true, nil } // SystemPowerCycle power cycles the system. func (c *Client) SystemPowerCycle(ctx context.Context) (ok bool, err error) { if err := c.SessionActive(); err != nil { return false, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } system, err := c.System() if err != nil { return false, err } if system.PowerState == schemas.OffPowerState { return false, fmt.Errorf("power cycle failed: Command not supported in present state: %v", system.PowerState) } system.DisableEtagMatch(c.disableEtagMatch) if _, err = system.Reset(schemas.ForceRestartResetType); err != nil { return false, errors.WithMessage(err, "power cycle failed") } return true, nil } // SystemPowerStatus returns the system power state. func (c *Client) SystemPowerStatus(ctx context.Context) (result string, err error) { if err := c.SessionActive(); err != nil { return result, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } system, err := c.System() if err != nil { return "", err } return string(system.PowerState), nil } // SystemForceOff powers off the system, without waiting for the OS to shutdown. func (c *Client) SystemForceOff(ctx context.Context) (ok bool, err error) { if err := c.SessionActive(); err != nil { return false, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } system, err := c.System() if err != nil { return false, err } if system.PowerState == schemas.OffPowerState { return true, nil } system.DisableEtagMatch(c.disableEtagMatch) if _, err = system.Reset(schemas.ForceOffResetType); err != nil { return false, err } return true, nil } // SendNMI tells the BMC to issue an NMI to the device func (c *Client) SendNMI(_ context.Context) error { if err := c.SessionActive(); err != nil { return errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } system, err := c.System() if err != nil { return err } _, err = system.Reset(schemas.NmiResetType) return err } ================================================ FILE: internal/redfishwrapper/sel.go ================================================ package redfishwrapper import ( "context" "encoding/json" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" "github.com/stmcginnis/gofish/schemas" ) // ClearSystemEventLog clears all of the LogServices logs func (c *Client) ClearSystemEventLog(ctx context.Context) (err error) { if err := c.SessionActive(); err != nil { return errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } chassis, err := c.client.Service.Chassis() if err != nil { return err } for _, c := range chassis { logServices, err := c.LogServices() if err != nil { return err } for _, logService := range logServices { _, err = logService.ClearLog("") if err != nil { return err } } } return nil } // GetSystemEventLog returns the SystemEventLogEntries func (c *Client) GetSystemEventLog(ctx context.Context) (entries [][]string, err error) { if err := c.SessionActive(); err != nil { return nil, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } managers, err := c.client.Service.Managers() if err != nil { return nil, err } for _, m := range managers { logServices, err := m.LogServices() if err != nil { return nil, err } for _, logService := range logServices { lentries, err := logService.Entries() if err != nil { return nil, err } for _, entry := range lentries { entries = append(entries, []string{ entry.ID, entry.Created, entry.Description, entry.Message, }) } } } return entries, nil } // GetSystemEventLogRaw returns the raw SEL func (c *Client) GetSystemEventLogRaw(ctx context.Context) (eventlog string, err error) { var allEntries []*schemas.LogEntry if err := c.SessionActive(); err != nil { return "", errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } managers, err := c.client.Service.Managers() if err != nil { return "", err } for _, m := range managers { logServices, err := m.LogServices() if err != nil { return "", err } for _, logService := range logServices { lentries, err := logService.Entries() if err != nil { return "", err } allEntries = append(allEntries, lentries...) } } rawEntries, err := json.Marshal(allEntries) if err != nil { return "", err } return string(rawEntries), nil } ================================================ FILE: internal/redfishwrapper/system.go ================================================ package redfishwrapper import ( "context" "fmt" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" "github.com/stmcginnis/gofish/schemas" ) // The methods here should be a thin wrapper so as to only guard the client from authentication failures. // AccountService gets the Redfish AccountService.d func (c *Client) AccountService() (*schemas.AccountService, error) { if err := c.SessionActive(); err != nil { return nil, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } return c.client.Service.AccountService() } // UpdateService gets the update service instance. func (c *Client) UpdateService() (*schemas.UpdateService, error) { if err := c.SessionActive(); err != nil { return nil, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } return c.client.Service.UpdateService() } // System gets the system matching c.systemName or when c.systemName is not set // and there is only one system it returns that system. func (c *Client) System() (*schemas.ComputerSystem, error) { if err := c.SessionActive(); err != nil { return nil, err } systems, err := c.client.Service.Systems() if err != nil { return nil, err } // If no system name is set and there is only one system, return it. // This is to handle backwards compatibility where we didn't require passing // a system name to the client. if c.systemName == "" && len(systems) == 1 && systems[0] != nil { return systems[0], nil } return c.matchingSystem(systems) } // Manager gets the manager instances of this service. It matches the manager // to the system name if one is set in the client. If no system name is set // and there is only one manager it returns that manager. Otherwise it returns // an error. func (c *Client) Manager(ctx context.Context) (*schemas.Manager, error) { if err := c.SessionActive(); err != nil { return nil, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } ms, err := c.client.Service.Managers() if err != nil { return nil, err } // If no system name is set and there is only one manager, return it. // This is to handle backwards compatibility where we didn't require passing // a system name to the client. if c.systemName == "" && len(ms) == 1 && ms[0] != nil { return ms[0], nil } for _, m := range ms { sys, err := m.ManagerForServers() if err != nil { continue } if _, err := c.matchingSystem(sys); err == nil { return m, nil } } return nil, fmt.Errorf("no matching redfish manager found for system: %s", c.systemName) } func (c *Client) Managers(ctx context.Context) ([]*schemas.Manager, error) { if err := c.SessionActive(); err != nil { return nil, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } return c.client.Service.Managers() } // Chassis gets the chassis instances managed by this service. func (c *Client) Chassis(ctx context.Context) ([]*schemas.Chassis, error) { if err := c.SessionActive(); err != nil { return nil, errors.Wrap(bmclibErrs.ErrNotAuthenticated, err.Error()) } return c.client.Service.Chassis() } func (c *Client) matchingSystem(systems []*schemas.ComputerSystem) (*schemas.ComputerSystem, error) { for _, s := range systems { if s != nil && s.Name == c.systemName { return s, nil } } return nil, fmt.Errorf("no matching redfish system found for system: %s", c.systemName) } ================================================ FILE: internal/redfishwrapper/system_test.go ================================================ package redfishwrapper import ( "testing" "github.com/stmcginnis/gofish/schemas" "github.com/stretchr/testify/assert" ) func TestMatchingSystem(t *testing.T) { tests := map[string]struct { client *Client systems []*schemas.ComputerSystem expectErr bool expectSystem *schemas.ComputerSystem }{ "finds matching system by name": { client: &Client{ systemName: "System1", }, systems: []*schemas.ComputerSystem{ { Entity: schemas.Entity{ Name: "System1", }, }, { Entity: schemas.Entity{ Name: "System2", }, }, }, expectErr: false, expectSystem: &schemas.ComputerSystem{ Entity: schemas.Entity{ Name: "System1", }, }, }, "no matching system found": { client: &Client{ systemName: "NonExistent", }, systems: []*schemas.ComputerSystem{ { Entity: schemas.Entity{ Name: "System1", }, }, }, expectErr: true, expectSystem: nil, }, "empty systems list": { client: &Client{ systemName: "System1", }, systems: []*schemas.ComputerSystem{}, expectErr: true, expectSystem: nil, }, "system name empty": { client: &Client{ systemName: "", }, systems: []*schemas.ComputerSystem{ { Entity: schemas.Entity{ Name: "System1", }, }, { Entity: schemas.Entity{ Name: "System2", }, }, }, expectErr: true, expectSystem: nil, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { result, err := tc.client.matchingSystem(tc.systems) if tc.expectErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, tc.expectSystem, result) } }) } } ================================================ FILE: internal/redfishwrapper/task.go ================================================ package redfishwrapper import ( "context" "fmt" "strings" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" "github.com/stmcginnis/gofish/schemas" ) var ( errUnexpectedTaskState = errors.New("unexpected task state") ) func (c *Client) Task(ctx context.Context, taskID string) (*schemas.Task, error) { tasks, err := c.Tasks(ctx) if err != nil { return nil, errors.Wrap(err, "error querying redfish tasks") } for _, t := range tasks { if t.ID != taskID { continue } return t, nil } return nil, bmclibErrs.ErrTaskNotFound } func (c *Client) TaskStatus(ctx context.Context, taskID string) (constants.TaskState, string, error) { task, err := c.Task(ctx, taskID) if err != nil { return "", "", errors.Wrap(err, "error querying redfish for taskID: "+taskID) } taskInfo := fmt.Sprintf( "id: %s, state: %s, status: %s", task.ID, task.TaskState, task.TaskStatus, ) // task message include information that help debug a cause of failure if msgs := c.taskMessagesAsString(task.Messages); msgs != "" { taskInfo += ", messages: " + msgs } s := c.ConvertTaskState(string(task.TaskState)) return s, taskInfo, nil } func (c *Client) taskMessagesAsString(messages []schemas.Message) string { if len(messages) == 0 { return "" } var found []string for _, m := range messages { if m.Message == "" { continue } found = append(found, m.Message) } return strings.Join(found, ",") } func (c *Client) ConvertTaskState(state string) constants.TaskState { switch strings.ToLower(state) { case "starting", "downloading", "downloaded", "scheduling": return constants.Initializing case "running", "stopping", "cancelling": return constants.Running case "pending", "new": return constants.Queued case "scheduled": return constants.PowerCycleHost case "interrupted", "killed", "exception", "cancelled", "suspended", "failed": return constants.Failed case "completed": return constants.Complete default: return constants.Unknown } } func (c *Client) TaskStateActive(state constants.TaskState) (bool, error) { switch state { case constants.Initializing, constants.Running, constants.Queued: return true, nil case constants.Complete, constants.Failed: return false, nil default: return false, errors.Wrap(errUnexpectedTaskState, string(state)) } } ================================================ FILE: internal/redfishwrapper/task_test.go ================================================ package redfishwrapper import ( "context" "net/http" "net/http/httptest" "net/url" "testing" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/stretchr/testify/assert" ) func TestConvertTaskState(t *testing.T) { testCases := []struct { testName string state string expected constants.TaskState }{ {"starting state", "starting", constants.Initializing}, {"downloading state", "downloading", constants.Initializing}, {"downloaded state", "downloaded", constants.Initializing}, {"scheduling state", "scheduling", constants.Initializing}, {"running state", "running", constants.Running}, {"stopping state", "stopping", constants.Running}, {"cancelling state", "cancelling", constants.Running}, {"pending state", "pending", constants.Queued}, {"new state", "new", constants.Queued}, {"scheduled state", "scheduled", constants.PowerCycleHost}, {"interrupted state", "interrupted", constants.Failed}, {"killed state", "killed", constants.Failed}, {"exception state", "exception", constants.Failed}, {"cancelled state", "cancelled", constants.Failed}, {"suspended state", "suspended", constants.Failed}, {"failed state", "failed", constants.Failed}, {"completed state", "completed", constants.Complete}, {"unknown state", "unknown_state", constants.Unknown}, } client := Client{} for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { result := client.ConvertTaskState(tc.state) assert.Equal(t, tc.expected, result) }) } } func TestTaskStateActive(t *testing.T) { testCases := []struct { testName string taskState constants.TaskState expected bool err error }{ {"active initializing", constants.Initializing, true, nil}, {"active running", constants.Running, true, nil}, {"active queued", constants.Queued, true, nil}, {"inactive complete", constants.Complete, false, nil}, {"inactive failed", constants.Failed, false, nil}, {"unknown state", "foobar", false, errUnexpectedTaskState}, } client := &Client{} for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { active, err := client.TaskStateActive(tc.taskState) if tc.err != nil { assert.ErrorIs(t, err, tc.err) return } if err != nil { t.Fatal(err) } assert.Equal(t, tc.expected, active) }) } } func TestTaskStatus(t *testing.T) { type hmap map[string]func(http.ResponseWriter, *http.Request) withHandler := func(s string, f func(http.ResponseWriter, *http.Request)) hmap { return hmap{ "/redfish/v1/": endpointFunc(t, "serviceroot.json"), "/redfish/v1/Systems": endpointFunc(t, "systems.json"), "/redfish/v1/TaskService": endpointFunc(t, "taskservice.json"), "/redfish/v1/TaskService/Tasks": endpointFunc(t, "tasks.json"), // "/redfish/v1/TaskService/Tasks/1": endpointFunc(t, "tasks_1.json"), // "/redfish/v1/TaskService/Tasks/2": endpointFunc(t, "tasks_2.json"), s: f, } } tests := map[string]struct { hmap hmap expectedState constants.TaskState expectedStatus string expectedErr error }{ "task in Initializing state": { hmap: withHandler( "/redfish/v1/TaskService/Tasks/1", endpointFunc(t, "tasks/tasks_1_starting.json"), ), expectedState: constants.Initializing, expectedStatus: "id: 1, state: Starting, status: OK", expectedErr: nil, }, "task in Running state": { hmap: withHandler( "/redfish/v1/TaskService/Tasks/1", endpointFunc(t, "tasks/tasks_1_running.json"), ), expectedState: constants.Running, expectedStatus: "id: 1, state: Running, status: OK", expectedErr: nil, }, "task in Queued state": { hmap: withHandler( "/redfish/v1/TaskService/Tasks/1", endpointFunc(t, "tasks/tasks_1_pending.json"), ), expectedState: constants.Queued, expectedStatus: "id: 1, state: Pending, status: OK", expectedErr: nil, }, "task in PowerCycleHost state": { hmap: withHandler( "/redfish/v1/TaskService/Tasks/1", endpointFunc(t, "tasks/tasks_1_scheduled.json"), ), expectedState: constants.PowerCycleHost, expectedStatus: "id: 1, state: Scheduled, status: OK", expectedErr: nil, }, "task in Failed state": { hmap: withHandler( "/redfish/v1/TaskService/Tasks/1", endpointFunc(t, "tasks/tasks_1_failed.json"), ), expectedState: constants.Failed, expectedStatus: "id: 1, state: Failed, status: OK", expectedErr: nil, }, "task in Complete state": { hmap: withHandler( "/redfish/v1/TaskService/Tasks/1", endpointFunc(t, "tasks/tasks_1_completed.json"), ), expectedState: constants.Complete, expectedStatus: "id: 1, state: Completed, status: OK", expectedErr: nil, }, "unknown task state": { hmap: withHandler( "/redfish/v1/TaskService/Tasks/1", endpointFunc(t, "tasks/tasks_1_unknown.json"), ), expectedState: constants.Unknown, expectedStatus: "id: 1, state: foobared, status: OK", expectedErr: nil, }, "failure case - no task found": { hmap: hmap{ "/redfish/v1/": endpointFunc(t, "serviceroot.json"), "/redfish/v1/Systems": endpointFunc(t, "systems.json"), "/redfish/v1/TaskService": endpointFunc(t, "taskservice.json"), "/redfish/v1/TaskService/Tasks": endpointFunc(t, "tasks.json"), }, expectedState: "", expectedStatus: "", expectedErr: bmclibErrs.ErrTaskNotFound, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { mux := http.NewServeMux() for endpoint, handler := range tc.hmap { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } ctx := context.Background() client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "", WithBasicAuthEnabled(true)) err = client.Open(ctx) if err != nil { t.Fatal(err) } state, status, err := client.TaskStatus(ctx, "1") if tc.expectedErr != nil { assert.ErrorContains(t, err, tc.expectedErr.Error()) return } assert.Nil(t, err) assert.Equal(t, tc.expectedState, state) assert.Equal(t, tc.expectedStatus, status) client.Close(context.Background()) }) } } func TestTask(t *testing.T) { type hmap map[string]func(http.ResponseWriter, *http.Request) handlers := func() hmap { return hmap{ "/redfish/v1/": endpointFunc(t, "serviceroot.json"), "/redfish/v1/Systems": endpointFunc(t, "systems.json"), "/redfish/v1/TaskService": endpointFunc(t, "taskservice.json"), "/redfish/v1/TaskService/Tasks": endpointFunc(t, "tasks.json"), "/redfish/v1/TaskService/Tasks/1": endpointFunc(t, "/tasks/tasks_1_completed.json"), "/redfish/v1/TaskService/Tasks/2": endpointFunc(t, "/tasks/tasks_2.json"), } } tests := map[string]struct { handlers hmap taskID string expectTaskStatus string expectTaskState string err error }{ "happy case - task 1": { handlers: handlers(), taskID: "1", expectTaskStatus: "OK", expectTaskState: "Completed", err: nil, }, "happy case - task 2": { handlers: handlers(), taskID: "2", expectTaskStatus: "OK", expectTaskState: "Completed", err: nil, }, "failure case - no task found": { handlers: handlers(), taskID: "3", err: bmclibErrs.ErrTaskNotFound, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { mux := http.NewServeMux() for endpoint, handler := range tc.handlers { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } ctx := context.Background() //os.Setenv("DEBUG_BMCLIB", "true") client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "", WithBasicAuthEnabled(true)) err = client.Open(ctx) if err != nil { t.Fatal(err) } got, err := client.Task(ctx, tc.taskID) if tc.err != nil { assert.ErrorContains(t, err, tc.err.Error()) return } assert.Nil(t, err) assert.NotNil(t, got) assert.Equal(t, tc.expectTaskStatus, string(got.TaskStatus)) assert.Equal(t, tc.expectTaskState, string(got.TaskState)) client.Close(context.Background()) }) } } ================================================ FILE: internal/redfishwrapper/virtual_media.go ================================================ package redfishwrapper import ( "context" "errors" "fmt" "slices" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/schemas" ) // getVirtualMedia retrieves virtual media resources by first checking the // Redfish Manager path and falling back to the System path if none are found. // // Some BMC implementations (e.g., Dell iDRAC) expose VirtualMedia under the // System resource (/redfish/v1/Systems/{SystemId}/VirtualMedia) rather than // the Manager resource (/redfish/v1/Managers/{ManagerId}/VirtualMedia). // Both locations are valid per the Redfish specification. func (c *Client) getVirtualMedia(ctx context.Context) ([]*schemas.VirtualMedia, error) { // Try Manager path first (standard Redfish location). m, err := c.Manager(ctx) if err == nil { vm, err := m.VirtualMedia() if err == nil && len(vm) > 0 { return vm, nil } } // Fallback to System path (Dell iDRAC and other implementations that // expose VirtualMedia under ComputerSystem per Redfish spec v1.12.0+). sys, err := c.System() if err == nil { vm, err := sys.VirtualMedia() if err == nil && len(vm) > 0 { return vm, nil } } return nil, errors.New("no virtual media found at Manager or System resource paths") } // SetVirtualMedia sets virtual media on the system. If mediaURL is empty, // matching media may be ejected. When multiple matching virtual media slots // exist, each slot is tried in order until one succeeds. func (c *Client) SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (bool, error) { var mediaKind schemas.VirtualMediaType switch kind { case "CD": mediaKind = schemas.CDVirtualMediaType case "Floppy": mediaKind = schemas.FloppyVirtualMediaType case "USBStick": mediaKind = schemas.USBStickVirtualMediaType case "DVD": mediaKind = schemas.DVDVirtualMediaType default: return false, errors.New("invalid media type") } virtualMedia, err := c.getVirtualMedia(ctx) if err != nil { return false, err } supportedMediaTypes := []string{} var slotErrors []error for _, vm := range virtualMedia { if !slices.Contains(vm.MediaTypes, mediaKind) { for _, mt := range vm.MediaTypes { supportedMediaTypes = append(supportedMediaTypes, string(mt)) } continue } if mediaURL == "" { // Only ejecting the media was requested. if *vm.Inserted && vm.SupportsMediaEject { if _, err := vm.EjectMedia(); err != nil { slotErrors = append(slotErrors, fmt.Errorf("%s: eject: %w", vm.ODataID, err)) continue } } return true, nil } // Ejecting the media before inserting new media makes the success rate of inserting the new media higher. if *vm.Inserted && vm.SupportsMediaEject { if _, err := vm.EjectMedia(); err != nil { slotErrors = append(slotErrors, fmt.Errorf("%s: eject before insert: %w", vm.ODataID, err)) continue } } if !vm.SupportsMediaInsert { slotErrors = append(slotErrors, fmt.Errorf("%s: does not support insert", vm.ODataID)) continue } insertMedia := schemas.VirtualMediaInsertMediaParameters{ Image: mediaURL, Inserted: gofish.ToRef(true), WriteProtected: gofish.ToRef(true), } if _, err := vm.InsertMedia(&insertMedia); err != nil { // Some BMCs (e.g., Supermicro X11SDV-4C-TLN2F) don't support the // Inserted and WriteProtected properties, so retry without them. insertMedia = schemas.VirtualMediaInsertMediaParameters{Image: mediaURL} if _, err := vm.InsertMedia(&insertMedia); err != nil { slotErrors = append(slotErrors, fmt.Errorf("%s: insert: %w", vm.ODataID, err)) continue } } return true, nil } if len(slotErrors) > 0 { return false, fmt.Errorf("all matching virtual media slots failed: %w", errors.Join(slotErrors...)) } return false, fmt.Errorf("not a supported media type: %s. supported media types: %v", kind, supportedMediaTypes) } func (c *Client) InsertedVirtualMedia(ctx context.Context) ([]string, error) { virtualMedia, err := c.getVirtualMedia(ctx) if err != nil { return nil, err } var inserted []string for _, media := range virtualMedia { if *media.Inserted { inserted = append(inserted, media.ID) } } return inserted, nil } ================================================ FILE: internal/redfishwrapper/virtual_media_test.go ================================================ package redfishwrapper import ( "context" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGetVirtualMedia(t *testing.T) { tests := map[string]struct { hfunc map[string]func(http.ResponseWriter, *http.Request) basicAuth bool expectCount int expectErr string }{ "manager path has virtual media": { // Standard case: VirtualMedia is found under Manager (e.g., HP iLO, Supermicro) hfunc: map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "serviceroot.json"), "/redfish/v1/Managers": endpointFunc(t, "managers.json"), "/redfish/v1/Managers/1": endpointFunc(t, "managers_1.json"), "/redfish/v1/Managers/1/VirtualMedia": endpointFunc(t, "404"), "/redfish/v1/Systems": endpointFunc(t, "systems.json"), "/redfish/v1/Systems/1": endpointFunc(t, "systems_1.json"), }, // managers_1.json has a VirtualMedia link, but our mock returns 404 for the collection. // This means Manager path returns 0 items, so fallback to System path. // systems_1.json doesn't have VirtualMedia link, so both fail. expectCount: 0, expectErr: "no virtual media found", }, "dell idrac - system path has virtual media": { // Dell iDRAC case: Manager has no VirtualMedia, System has VirtualMedia hfunc: map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "dell/serviceroot.json"), "/redfish/v1/Managers": endpointFunc(t, "dell/managers.json"), "/redfish/v1/Managers/iDRAC.Embedded.1": endpointFunc(t, "dell/manager.idrac.embedded.1.json"), "/redfish/v1/Systems": endpointFunc(t, "dell/systems.json"), "/redfish/v1/Systems/System.Embedded.1": endpointFunc(t, "dell/system.embedded.1.virtualmedia.json"), "/redfish/v1/Systems/System.Embedded.1/VirtualMedia": endpointFunc(t, "dell/virtualmedia_collection.json"), "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/1": endpointFunc(t, "dell/virtualmedia_1.json"), "/redfish/v1/Systems/System.Embedded.1/VirtualMedia/2": endpointFunc(t, "dell/virtualmedia_2.json"), }, basicAuth: true, expectCount: 2, expectErr: "", }, "no virtual media anywhere": { // Neither Manager nor System exposes VirtualMedia hfunc: map[string]func(http.ResponseWriter, *http.Request){ "/redfish/v1/": endpointFunc(t, "dell/serviceroot.json"), "/redfish/v1/Managers": endpointFunc(t, "dell/managers.json"), "/redfish/v1/Managers/iDRAC.Embedded.1": endpointFunc(t, "dell/manager.idrac.embedded.1.json"), "/redfish/v1/Systems": endpointFunc(t, "dell/systems.json"), "/redfish/v1/Systems/System.Embedded.1": endpointFunc(t, "dell/system.embedded.1.json"), }, basicAuth: true, expectCount: 0, expectErr: "no virtual media found", }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { mux := http.NewServeMux() for endpoint, handler := range tc.hfunc { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) require.NoError(t, err) ctx := context.Background() opts := []Option{} if tc.basicAuth { opts = append(opts, WithBasicAuthEnabled(true)) } client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "", opts...) err = client.Open(ctx) require.NoError(t, err) defer client.Close(ctx) vm, err := client.getVirtualMedia(ctx) if tc.expectErr != "" { assert.Error(t, err) assert.Contains(t, err.Error(), tc.expectErr) assert.Nil(t, vm) } else { assert.NoError(t, err) assert.Len(t, vm, tc.expectCount) } }) } } func TestSetVirtualMedia_DellSystemPath(t *testing.T) { // Test that SetVirtualMedia works with Dell iDRAC where VirtualMedia // is only available under the System resource path. // We test ejection (empty mediaURL) which only requires GET operations // and validates the full System path fallback flow. mux := http.NewServeMux() mux.HandleFunc("/redfish/v1/", endpointFunc(t, "dell/serviceroot.json")) mux.HandleFunc("/redfish/v1/Managers", endpointFunc(t, "dell/managers.json")) mux.HandleFunc("/redfish/v1/Managers/iDRAC.Embedded.1", endpointFunc(t, "dell/manager.idrac.embedded.1.json")) mux.HandleFunc("/redfish/v1/Systems", endpointFunc(t, "dell/systems.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1", endpointFunc(t, "dell/system.embedded.1.virtualmedia.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia", endpointFunc(t, "dell/virtualmedia_collection.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia/1", endpointFunc(t, "dell/virtualmedia_1.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia/2", endpointFunc(t, "dell/virtualmedia_2.json")) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) require.NoError(t, err) ctx := context.Background() client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "", WithBasicAuthEnabled(true)) err = client.Open(ctx) require.NoError(t, err) defer client.Close(ctx) // Test ejecting CD media via System path (empty URL = eject). // VirtualMedia fixtures have Inserted: false, so eject is a no-op success. ok, err := client.SetVirtualMedia(ctx, "CD", "") assert.NoError(t, err) assert.True(t, ok) } func TestInsertedVirtualMedia_DellSystemPath(t *testing.T) { // Test that InsertedVirtualMedia works when VirtualMedia is only // available under the System resource path (Dell iDRAC). mux := http.NewServeMux() mux.HandleFunc("/redfish/v1/", endpointFunc(t, "dell/serviceroot.json")) mux.HandleFunc("/redfish/v1/Managers", endpointFunc(t, "dell/managers.json")) mux.HandleFunc("/redfish/v1/Managers/iDRAC.Embedded.1", endpointFunc(t, "dell/manager.idrac.embedded.1.json")) mux.HandleFunc("/redfish/v1/Systems", endpointFunc(t, "dell/systems.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1", endpointFunc(t, "dell/system.embedded.1.virtualmedia.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia", endpointFunc(t, "dell/virtualmedia_collection.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia/1", endpointFunc(t, "dell/virtualmedia_1.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia/2", endpointFunc(t, "dell/virtualmedia_2.json")) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) require.NoError(t, err) ctx := context.Background() client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "", WithBasicAuthEnabled(true)) err = client.Open(ctx) require.NoError(t, err) defer client.Close(ctx) // Both VirtualMedia instances have Inserted: false, so should return empty inserted, err := client.InsertedVirtualMedia(ctx) assert.NoError(t, err) assert.Empty(t, inserted) } func TestSetVirtualMedia_SlotFallback(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/redfish/v1/", endpointFunc(t, "dell/serviceroot.json")) mux.HandleFunc("/redfish/v1/Managers", endpointFunc(t, "dell/managers.json")) mux.HandleFunc("/redfish/v1/Managers/iDRAC.Embedded.1", endpointFunc(t, "dell/manager.idrac.embedded.1.json")) mux.HandleFunc("/redfish/v1/Systems", endpointFunc(t, "dell/systems.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1", endpointFunc(t, "dell/system.embedded.1.virtualmedia.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia", endpointFunc(t, "dell/virtualmedia_collection.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia/1", endpointFunc(t, "dell/virtualmedia_1.json")) mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia/2", endpointFunc(t, "dell/virtualmedia_2.json")) // VirtualMedia/2 (first in collection) rejects InsertMedia with 500. mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia/2/Actions/VirtualMedia.InsertMedia", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) }, ) // VirtualMedia/1 (second in collection) accepts InsertMedia. mux.HandleFunc("/redfish/v1/Systems/System.Embedded.1/VirtualMedia/1/Actions/VirtualMedia.InsertMedia", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) }, ) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) require.NoError(t, err) ctx := context.Background() client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "", WithBasicAuthEnabled(true)) err = client.Open(ctx) require.NoError(t, err) defer client.Close(ctx) ok, err := client.SetVirtualMedia(ctx, "CD", "http://example.com/boot.iso") assert.NoError(t, err) assert.True(t, ok) } func TestSetVirtualMedia_InvalidMediaType(t *testing.T) { // Test that invalid media type returns an error before any Redfish calls mux := http.NewServeMux() mux.HandleFunc("/redfish/v1/", endpointFunc(t, "serviceroot.json")) mux.HandleFunc("/redfish/v1/Managers", endpointFunc(t, "managers.json")) mux.HandleFunc("/redfish/v1/Managers/1", endpointFunc(t, "managers_1.json")) mux.HandleFunc("/redfish/v1/Systems", endpointFunc(t, "systems.json")) mux.HandleFunc("/redfish/v1/Systems/1", endpointFunc(t, "systems_1.json")) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) require.NoError(t, err) ctx := context.Background() client := NewClient(parsedURL.Hostname(), parsedURL.Port(), "", "") err = client.Open(ctx) require.NoError(t, err) defer client.Close(ctx) ok, err := client.SetVirtualMedia(ctx, "InvalidType", "http://example.com/boot.iso") assert.Error(t, err) assert.False(t, ok) assert.Contains(t, err.Error(), "invalid media type") } ================================================ FILE: internal/sum/sum.go ================================================ package sum // SUM is Supermicro Update Manager // https://www.supermicro.com/en/solutions/management-software/supermicro-update-manager import ( "context" "os" "os/exec" "strings" ex "github.com/bmc-toolbox/bmclib/v2/internal/executor" "github.com/bmc-toolbox/common" "github.com/bmc-toolbox/common/config" "github.com/go-logr/logr" ) // Sum is a sum command executor object type Sum struct { Executor ex.Executor SumPath string Log logr.Logger Host string Username string Password string } // Option for setting optional Client values type Option func(*Sum) func WithSumPath(sumPath string) Option { return func(c *Sum) { c.SumPath = sumPath } } func WithLogger(log logr.Logger) Option { return func(c *Sum) { c.Log = log } } func New(host, user, pass string, opts ...Option) (*Sum, error) { sum := &Sum{ Host: host, Username: user, Password: pass, Log: logr.Discard(), } for _, opt := range opts { opt(sum) } var err error if sum.SumPath == "" { sum.SumPath, err = exec.LookPath("sum") if err != nil { return nil, err } } else { if _, err = os.Stat(sum.SumPath); err != nil { return nil, err } } e := ex.NewExecutor(sum.SumPath) e.SetEnv([]string{"LC_ALL=C.UTF-8"}) sum.Executor = e return sum, nil } // Open a connection to a BMC func (c *Sum) Open(ctx context.Context) (err error) { return nil } // Close a connection to a BMC func (c *Sum) Close(ctx context.Context) (err error) { return nil } func (s *Sum) run(ctx context.Context, command string, additionalArgs ...string) (output string, err error) { // TODO(splaspood) use a tmp file here (as sum supports) to read the password sumArgs := []string{"-i", s.Host, "-u", s.Username, "-p", s.Password, "-c", command} sumArgs = append(sumArgs, additionalArgs...) s.Log.V(9).WithValues( "sumArgs", sumArgs, ).Info("Calling sum") s.Executor.SetArgs(sumArgs) result, err := s.Executor.ExecWithContext(ctx) if err != nil { return string(result.Stderr), err } return string(result.Stdout), err } func (s *Sum) GetCurrentBiosCfg(ctx context.Context) (output string, err error) { return s.run(ctx, "GetCurrentBiosCfg") } func (s *Sum) LoadDefaultBiosCfg(ctx context.Context) (err error) { _, err = s.run(ctx, "LoadDefaultBiosCfg") return err } func (s *Sum) ChangeBiosCfg(ctx context.Context, cfgFile string, reboot bool) (err error) { args := []string{"--file", cfgFile} if reboot { args = append(args, "--reboot") } _, err = s.run(ctx, "ChangeBiosCfg", args...) return err } // GetBiosConfiguration return bios configuration func (s *Sum) GetBiosConfiguration(ctx context.Context) (biosConfig map[string]string, err error) { biosText, err := s.GetCurrentBiosCfg(ctx) if err != nil { return nil, err } // We need to call vcm here to take the XML returned by SUM and convert it into a simple map vcm, err := config.NewVendorConfigManager("xml", common.VendorSupermicro, map[string]string{}) if err != nil { return nil, err } err = vcm.Unmarshal(biosText) if err != nil { return nil, err } biosConfig, err = vcm.StandardConfig() if err != nil { return nil, err } return biosConfig, nil } // SetBiosConfiguration set bios configuration func (s *Sum) SetBiosConfiguration(ctx context.Context, biosConfig map[string]string) (err error) { vcm, err := config.NewVendorConfigManager("xml", common.VendorSupermicro, map[string]string{}) if err != nil { return err } for k, v := range biosConfig { switch { case k == "boot_mode": if err = vcm.BootMode(v); err != nil { return err } case k == "boot_order": if err = vcm.BootOrder(v); err != nil { return err } case k == "intel_sgx": if err = vcm.IntelSGX(v); err != nil { return err } case k == "secure_boot": switch v { case "Enabled": if err = vcm.SecureBoot(true); err != nil { return err } case "Disabled": if err = vcm.SecureBoot(false); err != nil { return err } } case k == "tpm": switch v { case "Enabled": if err = vcm.TPM(true); err != nil { return err } case "Disabled": if err = vcm.TPM(false); err != nil { return err } } case k == "smt": switch v { case "Enabled": if err = vcm.SMT(true); err != nil { return err } case "Disabled": if err = vcm.SMT(false); err != nil { return err } } case k == "sr_iov": switch v { case "Enabled": if err = vcm.SRIOV(true); err != nil { return err } case "Disabled": if err = vcm.SRIOV(false); err != nil { return err } } case strings.HasPrefix(k, "raw:"): // k = raw:Menu1,SubMenu1,SubMenuMenu1,SettingName pathStr := strings.TrimPrefix(k, "raw:") path := strings.Split(pathStr, ",") name := path[len(path)-1] path = path[:len(path)-1] vcm.Raw(name, v, path) } } xmlData, err := vcm.Marshal() if err != nil { return err } return s.SetBiosConfigurationFromFile(ctx, xmlData) } func (s *Sum) SetBiosConfigurationFromFile(ctx context.Context, cfg string) (err error) { // Open tmp file to hold cfg inputConfigTmpFile, err := os.CreateTemp("", "bmclib") if err != nil { return err } defer os.Remove(inputConfigTmpFile.Name()) _, err = inputConfigTmpFile.WriteString(cfg) if err != nil { return err } err = inputConfigTmpFile.Close() if err != nil { return err } return s.ChangeBiosCfg(ctx, inputConfigTmpFile.Name(), true) } // ResetBiosConfiguration reset bios configuration func (s *Sum) ResetBiosConfiguration(ctx context.Context) (err error) { return s.LoadDefaultBiosCfg(ctx) } ================================================ FILE: internal/sum/sum_test.go ================================================ package sum import ( "context" "os" "testing" ex "github.com/bmc-toolbox/bmclib/v2/internal/executor" ) func newFakeSum(t *testing.T, fixtureName string) *Sum { e := &Sum{ Executor: ex.NewFakeExecutor("sum"), } b, err := os.ReadFile("../../fixtures/internal/sum/" + fixtureName) if err != nil { t.Error(err) } e.Executor.SetStdout(b) return e } func TestExec_Run(t *testing.T) { // Create a new instance of Sum exec := newFakeSum(t, "GetBIOSInfo") // Create a new context ctx := context.Background() // Call the run function _, err := exec.run(ctx, "GetBIOSInfo") // Check the output and error if err != nil { t.Errorf("Expected no error, got: %v", err) } } func TestExec_SetBiosConfiguration(t *testing.T) { // Create a new context ctx := context.Background() // Define the BIOS configuration biosConfig := map[string]string{ "boot_mode": "UEFI", "boot_order": "UEFI", "intel_sgx": "Enabled", "secure_boot": "Enabled", "tpm": "Enabled", "smt": "Disabled", "sr_iov": "Enabled", "raw:Menu1,SubMenu1,SubMenuMenu1,SettingName": "Value", } exec := newFakeSum(t, "SetBiosConfiguration") // Call the SetBiosConfiguration function err := exec.SetBiosConfiguration(ctx, biosConfig) // Check for any errors if err != nil { t.Errorf("Expected no error, got: %v", err) } // Additional assertions can be added to verify the behavior of the function } func TestExec_GetBiosConfiguration(t *testing.T) { // Create a new context ctx := context.Background() exec := newFakeSum(t, "GetBiosConfiguration") // Call the SetBiosConfiguration function biosConfig, err := exec.GetBiosConfiguration(ctx) // Check for any errors if err != nil { t.Errorf("Expected no error, got: %v", err) } // Confirm boot_mode exists _, ok := biosConfig["boot_mode"] if !ok { t.Fail() } } ================================================ FILE: internal/utils.go ================================================ package internal import ( "unicode" ) // IsntLetterOrNumber check if the give rune is not a letter nor a number func IsntLetterOrNumber(c rune) bool { return !unicode.IsLetter(c) && !unicode.IsNumber(c) } func IsRoleValid(role string) bool { return role == "admin" || role == "user" || role == "operator" } func StringInSlice(str string, sl []string) bool { for _, s := range sl { if str == s { return true } } return false } ================================================ FILE: lint.mk ================================================ # BEGIN: lint-install github.com/bmc-toolbox/bmclib/v2 # http://github.com/tinkerbell/lint-install .PHONY: lint lint: _lint LINT_ARCH := $(shell uname -m) LINT_OS := $(shell uname) LINT_OS_LOWER := $(shell echo $(LINT_OS) | tr '[:upper:]' '[:lower:]') LINT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) # shellcheck and hadolint lack arm64 native binaries: rely on x86-64 emulation ifeq ($(LINT_OS),Darwin) ifeq ($(LINT_ARCH),arm64) LINT_ARCH=x86_64 endif endif LINTERS := FIXERS := GOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml GOLANGCI_LINT_VERSION ?= v1.61.0 GOLANGCI_LINT_BIN := $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH) $(GOLANGCI_LINT_BIN): mkdir -p $(LINT_ROOT)/out/linters rm -rf $(LINT_ROOT)/out/linters/golangci-lint-* curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LINT_ROOT)/out/linters $(GOLANGCI_LINT_VERSION) mv $(LINT_ROOT)/out/linters/golangci-lint $@ LINTERS += golangci-lint-lint golangci-lint-lint: $(GOLANGCI_LINT_BIN) find . -name go.mod -execdir "$(GOLANGCI_LINT_BIN)" run -c "$(GOLANGCI_LINT_CONFIG)" \; FIXERS += golangci-lint-fix golangci-lint-fix: $(GOLANGCI_LINT_BIN) find . -name go.mod -execdir "$(GOLANGCI_LINT_BIN)" run -c "$(GOLANGCI_LINT_CONFIG)" --fix \; .PHONY: _lint $(LINTERS) _lint: $(LINTERS) .PHONY: fix $(FIXERS) fix: $(FIXERS) # END: lint-install github.com/bmc-toolbox/bmclib/v2 ================================================ FILE: logging/logging.go ================================================ package logging import ( "os" "github.com/bombsimon/logrusr/v2" "github.com/go-logr/logr" "github.com/go-logr/zerologr" "github.com/rs/zerolog" "github.com/sirupsen/logrus" ) // DefaultLogger if no client logger is defined func DefaultLogger() logr.Logger { logrusLog := logrus.New() logrusLog.SetFormatter(&logrus.JSONFormatter{}) logrusLog.SetOutput(os.Stdout) switch os.Getenv("BMCLIB_LOG_LEVEL") { case "debug": logrusLog.SetLevel(logrus.DebugLevel) case "trace": logrusLog.SetLevel(logrus.TraceLevel) logrusLog.SetReportCaller(true) default: logrusLog.SetLevel(logrus.InfoLevel) } return logrusr.New(logrusLog) } // ZeroLogger is a logr.Logger implementation that uses zerolog. // This logger handles nested structs better than the logrus implementation. func ZeroLogger(level string) logr.Logger { zl := zerolog.New(os.Stdout) zl = zl.With().Caller().Timestamp().Logger() var l zerolog.Level switch level { case "debug": l = zerolog.DebugLevel default: l = zerolog.InfoLevel } zl = zl.Level(l) return zerologr.New(&zl) } ================================================ FILE: option.go ================================================ package bmclib import ( "context" "crypto/x509" "net/http" "time" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/bmc-toolbox/bmclib/v2/providers/homeassistant" "github.com/bmc-toolbox/bmclib/v2/providers/rpc" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" oteltrace "go.opentelemetry.io/otel/trace" ) // Option for setting optional Client values type Option func(*Client) // WithLogger sets the logger func WithLogger(logger logr.Logger) Option { return func(args *Client) { args.Logger = logger } } // WithRegistry sets the Registry func WithRegistry(registry *registrar.Registry) Option { return func(args *Client) { args.Registry = registry } } // WithSecureTLS enforces trusted TLS connections, with an optional CA certificate pool. // Using this option with an nil pool uses the system CAs. func WithSecureTLS(rootCAs *x509.CertPool) Option { return func(args *Client) { args.httpClientSetupFuncs = append(args.httpClientSetupFuncs, httpclient.SecureTLSOption(rootCAs)) } } // WithHTTPClient sets an http client func WithHTTPClient(c *http.Client) Option { return func(args *Client) { args.httpClient = c } } // WithPerProviderTimeout sets the timeout when interacting with a BMC. // This timeout value is applied per provider. // When not defined and a context with a timeout is passed to a method, the default timeout // will be the context timeout duration divided by the number of providers in the registry, // meaning, the len(Client.Registry.Drivers). // If this per provider timeout is not defined and no context timeout is defined, // the defaultConnectTimeout is used. func WithPerProviderTimeout(timeout time.Duration) Option { return func(args *Client) { args.perProviderTimeout = func(context.Context) time.Duration { return timeout } } } func WithIpmitoolCipherSuite(cipherSuite string) Option { return func(args *Client) { args.providerConfig.ipmitool.CipherSuite = cipherSuite } } func WithIpmitoolPort(port string) Option { return func(args *Client) { args.providerConfig.ipmitool.Port = port } } func WithIpmitoolPath(path string) Option { return func(args *Client) { args.providerConfig.ipmitool.IpmitoolPath = path } } func WithAsrockrackHTTPClient(httpClient *http.Client) Option { return func(args *Client) { args.providerConfig.asrock.HttpClient = httpClient } } func WithAsrockrackPort(port string) Option { return func(args *Client) { args.providerConfig.asrock.Port = port } } func WithRedfishHTTPClient(httpClient *http.Client) Option { return func(args *Client) { args.providerConfig.gofish.HttpClient = httpClient } } func WithRedfishPort(port string) Option { return func(args *Client) { args.providerConfig.gofish.Port = port } } // WithRedfishVersionsNotCompatible sets the list of incompatible redfish versions. // // With this option set, The bmclib.Registry.FilterForCompatible(ctx) method will not proceed on // devices with the given redfish version(s). func WithRedfishVersionsNotCompatible(versions []string) Option { return func(args *Client) { args.providerConfig.gofish.VersionsNotCompatible = append(args.providerConfig.gofish.VersionsNotCompatible, versions...) } } func WithRedfishUseBasicAuth(useBasicAuth bool) Option { return func(args *Client) { args.providerConfig.gofish.UseBasicAuth = useBasicAuth } } func WithRedfishEtagMatchDisabled(d bool) Option { return func(args *Client) { args.providerConfig.gofish.DisableEtagMatch = d } } func WithRedfishSystemName(name string) Option { return func(args *Client) { args.providerConfig.gofish.SystemName = name } } func WithIntelAMTHostScheme(hostScheme string) Option { return func(args *Client) { args.providerConfig.intelamt.HostScheme = hostScheme } } func WithIntelAMTPort(port uint32) Option { return func(args *Client) { args.providerConfig.intelamt.Port = port } } // WithDellRedfishVersionsNotCompatible sets the list of incompatible redfish versions. // // With this option set, The bmclib.Registry.FilterForCompatible(ctx) method will not proceed on // devices with the given redfish version(s). func WithDellRedfishVersionsNotCompatible(versions []string) Option { return func(args *Client) { args.providerConfig.dell.VersionsNotCompatible = append(args.providerConfig.dell.VersionsNotCompatible, versions...) } } func WithDellRedfishUseBasicAuth(useBasicAuth bool) Option { return func(args *Client) { args.providerConfig.dell.UseBasicAuth = useBasicAuth } } func WithRPCOpt(opt rpc.Provider) Option { return func(args *Client) { args.providerConfig.rpc = opt } } func WithHomeAssistantOpt(opt homeassistant.Config) Option { return func(args *Client) { args.providerConfig.homeassistant = opt } } // WithTracerProvider specifies a tracer provider to use for creating a tracer. // If none is specified a noop tracerprovider is used. func WithTracerProvider(provider oteltrace.TracerProvider) Option { return func(args *Client) { if provider != nil { args.traceprovider = provider } } } ================================================ FILE: providers/asrockrack/asrockrack.go ================================================ package asrockrack import ( "context" "crypto/x509" "fmt" "net/http" "strings" "github.com/bmc-toolbox/bmclib/v2/constants" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/bmc-toolbox/common" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" "github.com/pkg/errors" ) const ( // ProviderName for the provider implementation ProviderName = "asrockrack" // ProviderProtocol for the provider implementation ProviderProtocol = "vendorapi" E3C256D4ID_NL = "E3C256D4ID-NL" E3C246D4ID_NL = "E3C246D4ID-NL" E3C246D4I_NL = "E3C246D4I-NL" ) var ( // Features implemented by asrockrack https Features = registrar.Features{ providers.FeaturePostCodeRead, providers.FeatureBmcReset, providers.FeatureUserCreate, providers.FeatureUserUpdate, providers.FeatureFirmwareUpload, providers.FeatureFirmwareInstallUploaded, providers.FeatureFirmwareTaskStatus, providers.FeatureFirmwareInstallSteps, providers.FeatureInventoryRead, providers.FeaturePowerSet, providers.FeaturePowerState, } ) // ASRockRack holds the status and properties of a connection to a asrockrack bmc type ASRockRack struct { ip string username string password string deviceModel string loginSession *loginSession httpClient *http.Client resetRequired bool // Indicates if the BMC requires a reset skipLogout bool // A Close() / httpsLogout() request is ignored if the BMC was just flashed - since the sessions are terminated either way log logr.Logger httpClientSetupFuncs []func(*http.Client) } type Config struct { Port string HttpClient *http.Client } // ASRockOption is a type that can configure an *ASRockRack type ASRockOption func(*ASRockRack) // WithSecureTLS enforces trusted TLS connections, with an optional CA certificate pool. // Using this option with an nil pool uses the system CAs. func WithSecureTLS(rootCAs *x509.CertPool) ASRockOption { return func(r *ASRockRack) { r.httpClientSetupFuncs = append(r.httpClientSetupFuncs, httpclient.SecureTLSOption(rootCAs)) } } // WithHTTPClient sets an HTTP client on the ASRockRack func WithHTTPClient(c *http.Client) ASRockOption { return func(ar *ASRockRack) { ar.httpClient = c } } // New returns a new ASRockRack instance ready to be used func New(ip string, username string, password string, log logr.Logger) *ASRockRack { return NewWithOptions(ip, username, password, log) } // NewWithOptions returns a new ASRockRack instance with options ready to be used func NewWithOptions(ip string, username string, password string, log logr.Logger, opts ...ASRockOption) *ASRockRack { r := &ASRockRack{ ip: ip, username: username, password: password, log: log, loginSession: &loginSession{}, } for _, opt := range opts { opt(r) } if r.httpClient == nil { r.httpClient = httpclient.Build(r.httpClientSetupFuncs...) } else { for _, setupFunc := range r.httpClientSetupFuncs { setupFunc(r.httpClient) } } return r } func (a *ASRockRack) Name() string { return ProviderName } // Open a connection to a BMC, implements the Opener interface func (a *ASRockRack) Open(ctx context.Context) (err error) { if err := a.httpsLogin(ctx); err != nil { return err } return a.supported(ctx) } func (a *ASRockRack) supported(ctx context.Context) error { supported := []string{ E3C256D4ID_NL, E3C246D4ID_NL, E3C246D4I_NL, } if a.deviceModel == "" { device := common.NewDevice() device.Metadata = map[string]string{} err := a.fruAttributes(ctx, &device) if err != nil { return errors.Wrap(err, "failed to identify device model") } if device.Model == "" { return errors.Wrap(err, "failed to identify device model - empty model attribute") } a.deviceModel = device.Model } for _, s := range supported { if strings.EqualFold(a.deviceModel, s) { return nil } } return fmt.Errorf("device model not supported: %s", a.deviceModel) } // Close a connection to a BMC, implements the Closer interface func (a *ASRockRack) Close(ctx context.Context) (err error) { if a.skipLogout { return nil } return a.httpsLogout(ctx) } // CheckCredentials verify whether the credentials are valid or not func (a *ASRockRack) CheckCredentials(ctx context.Context) (err error) { return a.httpsLogin(ctx) } func (a *ASRockRack) PostCode(ctx context.Context) (status string, code int, err error) { postInfo, err := a.postCodeInfo(ctx) if err != nil { return status, code, err } code = postInfo.PostData status, exists := knownPOSTCodes[code] if !exists { status = constants.POSTCodeUnknown } return status, code, nil } ================================================ FILE: providers/asrockrack/asrockrack_test.go ================================================ package asrockrack import ( "context" "os" "testing" "time" "gopkg.in/go-playground/assert.v1" ) func TestHttpLogin(t *testing.T) { err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } assert.Equal(t, "l5L29IP7", aClient.loginSession.CSRFToken) } func TestClose(t *testing.T) { err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login setup: %s", err.Error()) } err = aClient.httpsLogout(context.TODO()) if err != nil { t.Errorf("logout: %s", err.Error()) } } func TestFirwmwareUpdateBMC(t *testing.T) { err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } upgradeFile := "/tmp/dummy-E3C246D4I-NL_L0.01.00.ima" _, err = os.Create(upgradeFile) if err != nil { t.Errorf("create file: %s", err.Error()) } fh, err := os.Open(upgradeFile) if err != nil { t.Errorf("file open: %s", err.Error()) } defer fh.Close() ctx, cancel := context.WithTimeout(context.TODO(), time.Minute*15) defer cancel() err = aClient.firmwareUploadBMC(ctx, fh) if err != nil { t.Errorf("upload: %s", err.Error()) } } ================================================ FILE: providers/asrockrack/firmware.go ================================================ package asrockrack import ( "context" "fmt" "os" "strings" "time" "github.com/pkg/errors" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/bmc-toolbox/bmclib/v2/internal" "github.com/bmc-toolbox/common" ) const ( versionStrError = -1 versionStrMatch = 0 versionStrMismatch = 1 versionStrEmpty = 2 ) // bmc client interface implementations methods func (a *ASRockRack) FirmwareInstallSteps(ctx context.Context, component string) ([]constants.FirmwareInstallStep, error) { if err := a.supported(ctx); err != nil { return nil, bmclibErrs.NewErrUnsupportedHardware(err.Error()) } switch strings.ToUpper(component) { case common.SlugBMC: return []constants.FirmwareInstallStep{ constants.FirmwareInstallStepUpload, constants.FirmwareInstallStepInstallUploaded, constants.FirmwareInstallStepInstallStatus, constants.FirmwareInstallStepResetBMCPostInstall, constants.FirmwareInstallStepResetBMCOnInstallFailure, }, nil } return nil, errors.Wrap(bmclibErrs.ErrFirmwareUpload, "component unsupported: "+component) } func (a *ASRockRack) FirmwareUpload(ctx context.Context, component string, file *os.File) (taskID string, err error) { switch strings.ToUpper(component) { case common.SlugBIOS: return "", a.firmwareUploadBIOS(ctx, file) case common.SlugBMC: return "", a.firmwareUploadBMC(ctx, file) } return "", errors.Wrap(bmclibErrs.ErrFirmwareUpload, "component unsupported: "+component) } func (a *ASRockRack) firmwareUploadBMC(ctx context.Context, file *os.File) error { // // expect atleast 5 minutes left in the deadline to proceed with the upload d, _ := ctx.Deadline() if time.Until(d) < 5*time.Minute { return errors.New("remaining context deadline insufficient to perform update: " + time.Until(d).String()) } // Beware: this locks some capabilities, e.g. the access to fruAttributes a.log.V(2).WithValues("step", "1/4").Info("set device to flash mode, takes a minute...") err := a.setFlashMode(ctx) if err != nil { return errors.Wrap( bmclibErrs.ErrFirmwareUpload, "failed in step 1/3 - set device to flash mode: "+err.Error(), ) } var fwEndpoint string switch a.deviceModel { // E3C256D4ID-NL calls a different endpoint for firmware upload case "E3C256D4ID-NL": fwEndpoint = "api/maintenance/firmware/firmware" default: fwEndpoint = "api/maintenance/firmware" } a.log.V(2).WithValues("step", "2/4").Info("upload BMC firmware image to " + fwEndpoint) err = a.uploadFirmware(ctx, fwEndpoint, file) if err != nil { return errors.Wrap( bmclibErrs.ErrFirmwareUpload, "failed in step 2/3 - upload BMC firmware image: "+err.Error(), ) } a.log.V(2).WithValues("step", "3/4").Info("verify uploaded BMC firmware") err = a.verifyUploadedFirmware(ctx) if err != nil { return errors.Wrap( bmclibErrs.ErrFirmwareUpload, "failed in step 3/3 - verify uploaded BMC firmware: "+err.Error(), ) } return nil } func (a *ASRockRack) firmwareUploadBIOS(ctx context.Context, file *os.File) error { a.log.V(2).WithValues("step", "1/3").Info("upload BIOS firmware image") err := a.uploadFirmware(ctx, "api/asrr/maintenance/BIOS/firmware", file) if err != nil { return errors.Wrap( bmclibErrs.ErrFirmwareUpload, "failed in step 1/3 - upload BIOS firmware image: "+err.Error(), ) } a.log.V(2).WithValues("step", "2/3").Info("set BIOS preserve flash configuration") err = a.biosUpgradeConfiguration(ctx) if err != nil { return errors.Wrap( bmclibErrs.ErrFirmwareUpload, "failed in step 2/3 - set flash configuration: "+err.Error(), ) } // 3. run upgrade a.log.V(2).WithValues("step", "3/3").Info("proceed with BIOS firmware install") err = a.upgradeBIOS(ctx) if err != nil { return errors.Wrap( bmclibErrs.ErrFirmwareUpload, "failed in step 3/3 - proceed with BIOS firmware install: "+err.Error(), ) } return nil } func (a *ASRockRack) FirmwareInstallUploaded(ctx context.Context, component, uploadTaskID string) (installTaskID string, err error) { switch strings.ToUpper(component) { case common.SlugBIOS: return "", a.firmwareInstallUploadedBIOS(ctx) case common.SlugBMC: return "", a.firmwareInstallUploadedBMC(ctx) } return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) } // firmwareInstallUploadedBIOS uploads and installs firmware for the BMC component func (a *ASRockRack) firmwareInstallUploadedBIOS(ctx context.Context) error { // 4. Run the upgrade - preserving current config a.log.V(2).WithValues("step", "install").Info("proceed with BIOS firmware install, preserve current configuration") err := a.upgradeBIOS(ctx) if err != nil { return errors.Wrap( bmclibErrs.ErrFirmwareInstallUploaded, "failed in step 4/4 - proceed with BMC firmware install: "+err.Error(), ) } return nil } // firmwareInstallUploadedBMC uploads and installs firmware for the BMC component func (a *ASRockRack) firmwareInstallUploadedBMC(ctx context.Context) error { // 4. Run the upgrade - preserving current config a.log.V(2).WithValues("step", "install").Info("proceed with BMC firmware install, preserve current configuration") err := a.upgradeBMC(ctx) if err != nil { return errors.Wrap( bmclibErrs.ErrFirmwareInstallUploaded, "failed in step 4/4 - proceed with BMC firmware install"+err.Error(), ) } return nil } // FirmwareTaskStatus returns the status of a firmware related task queued on the BMC. func (a *ASRockRack) FirmwareTaskStatus(ctx context.Context, kind constants.FirmwareInstallStep, component, taskID, installVersion string) (state constants.TaskState, status string, err error) { component = strings.ToUpper(component) switch component { case common.SlugBIOS, common.SlugBMC: return a.firmwareUpdateStatus(ctx, component, installVersion) default: return "", "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "component unsupported: "+component) } } // firmwareUpdateBIOSStatus returns the BIOS firmware install status func (a *ASRockRack) firmwareUpdateStatus(ctx context.Context, component string, installVersion string) (state constants.TaskState, status string, err error) { var endpoint string component = strings.ToUpper(component) switch component { case common.SlugBIOS: endpoint = "api/asrr/maintenance/BIOS/flash-progress" case common.SlugBMC: endpoint = "api/maintenance/firmware/flash-progress" default: return "", "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "component unsupported: "+component) } // 1. query the flash progress endpoint // // once an update completes/fails this endpoint will return 500 progress, err := a.flashProgress(ctx, endpoint) if err != nil { a.log.V(3).Error(err, "bmc query for install progress returned error: ") } if progress != nil { status = fmt.Sprintf("action: %s, progress: %s", progress.Action, progress.Progress) switch progress.State { case 0: return constants.Running, status, nil case 1: // "Flashing To be done" return constants.Queued, status, nil case 2: return constants.Complete, status, nil default: a.log.V(3).WithValues("state", progress.State).Info("warn", "bmc returned unknown flash progress state") } } // 2. query the firmware info endpoint to determine the update status // // at this point the flash-progress endpoint isn't returning useful information var installStatus int installStatus, err = a.versionInstalled(ctx, component, installVersion) if err != nil { return "", "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, err.Error()) } switch installStatus { case versionStrMatch: if progress == nil { // TODO: we should pass the force parameter to firmwareUpdateStatus, // so that we can know if we expect a version change or not a.log.V(3).Info("Nil progress + no version change -> unknown") return constants.Unknown, status, nil } return constants.Complete, status, nil case versionStrEmpty: return constants.Unknown, status, nil case versionStrMismatch: return constants.Running, status, nil } return constants.Unknown, status, nil } // versionInstalled returns int values on the status of the firmware version install // // - 0 indicates the given version parameter matches the version installed // - 1 indicates the given version parameter does not match the version installed // - 2 the version parameter returned from the BMC is empty (which means the BMC needs a reset) func (a *ASRockRack) versionInstalled(ctx context.Context, component, version string) (status int, err error) { component = strings.ToUpper(component) if !internal.StringInSlice(component, []string{common.SlugBIOS, common.SlugBMC}) { return versionStrError, errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) } fwInfo, err := a.firmwareInfo(ctx) if err != nil { err = errors.Wrap(err, "error querying for firmware info: ") a.log.V(3).Info("warn", err.Error()) return versionStrError, err } var installed string switch component { case common.SlugBIOS: installed = fwInfo.BIOSVersion case common.SlugBMC: installed = fwInfo.BMCVersion } // version match if strings.EqualFold(installed, version) { return versionStrMatch, nil } // fwinfo returned an empty string for firmware revision // this indicates the BMC is out of sync with the firmware versions installed if strings.TrimSpace(installed) == "" { return versionStrEmpty, nil } return 1, nil } ================================================ FILE: providers/asrockrack/firmware_update.md ================================================ ### BMC Flashing a BMC firmware seems to be a multi step process 1. PUT /api/maintenance/flash no payload (seems to set the device to be in flash mode or such) 200 OK - takes about a minute to return 2. POST /api/maintenance/firmware Content-Type: multipart/form-data ------WebKitFormBoundaryESKCgdjyLnqUPHBK Content-Disposition: form-data; name="fwimage"; filename="E3C246D4I-NL_L0.01.00.ima" Content-Type: application/octet-stream ------WebKitFormBoundaryESKCgdjyLnqUPHBK-- . response - '{"cc": 0}' - successful upload 3. GET /api/maintenance/firmware/verification 500 - Bad firmware payload -> invoke reset 200 - OK [ { "id": 1, "current_image_name": "ast2500e", "current_image_version1": "0.01.00", "current_image_version2": "", "new_image_version": "0.03.00", "section_status": 0, "verification_status": 5 } ] 4. If verificaion fails OR firmware update progress is at 100% done - invoke reset GET /api/maintenance/reset 200 OK 5. PUT /api/maintenance/firmware/upgrade payload {"preserve_config":1,"preserve_network":0,"preserve_user":0,"flash_status":1} 200 OK response - same as payload 6. GET https://10.230.148.171/api/maintenance/firmware/flash-progress { "id": 1, "action": "Flashing...", "progress": "12% done ", "state": 0 } { "id": 1, "action": "Flashing...", "progress": "100% done", "state": 0 } ### BIOS 1. POST api/asrr/maintenance/BIOS/firmware multipart payload: ------WebKitFormBoundaryBet48KCtZK4gBlQz Content-Disposition: form-data; name="fwimage"; filename="E6D4INL2.07B" Content-Type: application/octet-stream ------WebKitFormBoundaryBet48KCtZK4gBlQz-- 2. POST api/asrr/maintenance/BIOS/configuration payload {"action":"2"} 200 OK {"response": 1} 3. POST api/asrr/maintenance/BIOS/upgrade payload {action: 3} 200 oK 4. GET api/asrr/maintenance/BIOS/flash-progress ================================================ FILE: providers/asrockrack/fixtures/E3C246D4I-NL/sensors.json ================================================ [ { "id": 1, "sensor_number": 1, "name": "3VSB", "owner_id": 32, "owner_lun": 0, "raw_reading": 112.000000, "type": "voltage", "type_number": 2, "reading": 3.360000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 2.820000, "lower_critical_threshold": 2.970000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 3.630000, "higher_non_recoverable_threshold": 3.780000, "accessible": 0, "unit": "V" }, { "id": 2, "sensor_number": 2, "name": "5VSB", "owner_id": 32, "owner_lun": 0, "raw_reading": 101.000000, "type": "voltage", "type_number": 2, "reading": 5.050000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 4.250000, "lower_critical_threshold": 4.500000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 5.500000, "higher_non_recoverable_threshold": 5.750000, "accessible": 0, "unit": "V" }, { "id": 3, "sensor_number": 3, "name": "VCORE", "owner_id": 32, "owner_lun": 0, "raw_reading": 64.000000, "type": "voltage", "type_number": 2, "reading": 0.640000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 12336, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 1.890000, "higher_non_recoverable_threshold": 1.980000, "accessible": 0, "unit": "V" }, { "id": 4, "sensor_number": 4, "name": "VCCSA", "owner_id": 32, "owner_lun": 0, "raw_reading": 105.000000, "type": "voltage", "type_number": 2, "reading": 1.050000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 0.890000, "lower_critical_threshold": 0.950000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 1.160000, "higher_non_recoverable_threshold": 1.210000, "accessible": 0, "unit": "V" }, { "id": 5, "sensor_number": 5, "name": "VCCM", "owner_id": 32, "owner_lun": 0, "raw_reading": 120.000000, "type": "voltage", "type_number": 2, "reading": 1.200000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 1.020000, "lower_critical_threshold": 1.080000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 1.320000, "higher_non_recoverable_threshold": 1.380000, "accessible": 0, "unit": "V" }, { "id": 6, "sensor_number": 6, "name": "1.05V_PCH", "owner_id": 32, "owner_lun": 0, "raw_reading": 105.000000, "type": "voltage", "type_number": 2, "reading": 1.050000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 0.890000, "lower_critical_threshold": 0.950000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 1.160000, "higher_non_recoverable_threshold": 1.210000, "accessible": 0, "unit": "V" }, { "id": 7, "sensor_number": 7, "name": "VCCIO", "owner_id": 32, "owner_lun": 0, "raw_reading": 95.000000, "type": "voltage", "type_number": 2, "reading": 0.950000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 0.810000, "lower_critical_threshold": 0.860000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 1.050000, "higher_non_recoverable_threshold": 1.090000, "accessible": 0, "unit": "V" }, { "id": 8, "sensor_number": 9, "name": "VPPM", "owner_id": 32, "owner_lun": 0, "raw_reading": 125.000000, "type": "voltage", "type_number": 2, "reading": 2.500000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 2.200000, "lower_critical_threshold": 2.320000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 2.840000, "higher_non_recoverable_threshold": 2.960000, "accessible": 0, "unit": "V" }, { "id": 9, "sensor_number": 12, "name": "BAT", "owner_id": 32, "owner_lun": 0, "raw_reading": 96.000000, "type": "voltage", "type_number": 2, "reading": 2.880000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 2.550000, "lower_critical_threshold": 2.700000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 3.300000, "higher_non_recoverable_threshold": 3.450000, "accessible": 0, "unit": "V" }, { "id": 10, "sensor_number": 13, "name": "3V", "owner_id": 32, "owner_lun": 0, "raw_reading": 111.000000, "type": "voltage", "type_number": 2, "reading": 3.330000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 2.820000, "lower_critical_threshold": 2.970000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 3.630000, "higher_non_recoverable_threshold": 3.780000, "accessible": 0, "unit": "V" }, { "id": 11, "sensor_number": 14, "name": "5V", "owner_id": 32, "owner_lun": 0, "raw_reading": 101.000000, "type": "voltage", "type_number": 2, "reading": 5.050000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 4.250000, "lower_critical_threshold": 4.500000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 5.500000, "higher_non_recoverable_threshold": 5.750000, "accessible": 0, "unit": "V" }, { "id": 12, "sensor_number": 15, "name": "12V", "owner_id": 32, "owner_lun": 0, "raw_reading": 122.000000, "type": "voltage", "type_number": 2, "reading": 12.200000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 13878, "lower_non_recoverable_threshold": 10.200000, "lower_critical_threshold": 10.800000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 13.200000, "higher_non_recoverable_threshold": 13.800000, "accessible": 0, "unit": "V" }, { "id": 13, "sensor_number": 48, "name": "MB Temp", "owner_id": 32, "owner_lun": 0, "raw_reading": 30.000000, "type": "temperature", "type_number": 1, "reading": 30.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 6168, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 54.000000, "higher_critical_threshold": 55.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "°C" }, { "id": 14, "sensor_number": 50, "name": "TR1 Temp", "owner_id": 32, "owner_lun": 0, "raw_reading": 0.000000, "type": "temperature", "type_number": 1, "reading": 0.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 2056, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 65.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 213, "unit": "°C" }, { "id": 15, "sensor_number": 51, "name": "CPU Temp", "owner_id": 32, "owner_lun": 0, "raw_reading": 28.000000, "type": "temperature", "type_number": 1, "reading": 28.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 6168, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 99.000000, "higher_critical_threshold": 100.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "°C" }, { "id": 16, "sensor_number": 53, "name": "PCH Temp", "owner_id": 32, "owner_lun": 0, "raw_reading": 36.000000, "type": "temperature", "type_number": 1, "reading": 36.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 6168, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 99.000000, "higher_critical_threshold": 100.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "°C" }, { "id": 17, "sensor_number": 96, "name": "IPB FAN1", "owner_id": 32, "owner_lun": 0, "raw_reading": 26.000000, "type": "fan", "type_number": 4, "reading": 5200.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 257, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 200.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "RPM" }, { "id": 18, "sensor_number": 97, "name": "IPB FAN2", "owner_id": 32, "owner_lun": 0, "raw_reading": 26.000000, "type": "fan", "type_number": 4, "reading": 5200.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 257, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 200.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "RPM" }, { "id": 19, "sensor_number": 98, "name": "IPB FAN3", "owner_id": 32, "owner_lun": 0, "raw_reading": 26.000000, "type": "fan", "type_number": 4, "reading": 5200.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 257, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 200.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "RPM" }, { "id": 20, "sensor_number": 99, "name": "IPB FAN4", "owner_id": 32, "owner_lun": 0, "raw_reading": 26.000000, "type": "fan", "type_number": 4, "reading": 5200.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 257, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 200.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "RPM" }, { "id": 21, "sensor_number": 100, "name": "IPB FAN5", "owner_id": 32, "owner_lun": 0, "raw_reading": 26.000000, "type": "fan", "type_number": 4, "reading": 5200.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 257, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 200.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "RPM" }, { "id": 22, "sensor_number": 101, "name": "IPB FAN6", "owner_id": 32, "owner_lun": 0, "raw_reading": 26.000000, "type": "fan", "type_number": 4, "reading": 5200.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 257, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 200.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "RPM" }, { "id": 23, "sensor_number": 102, "name": "IPB FAN7", "owner_id": 32, "owner_lun": 0, "raw_reading": 26.000000, "type": "fan", "type_number": 4, "reading": 5200.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 257, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 200.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "RPM" }, { "id": 24, "sensor_number": 103, "name": "IPB FAN8", "owner_id": 32, "owner_lun": 0, "raw_reading": 26.000000, "type": "fan", "type_number": 4, "reading": 5200.000000, "sensor_state": 1, "discrete_state": 0, "settable_readable_threshMask": 257, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 200.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "RPM" }, { "id": 25, "sensor_number": 145, "name": "CPU_PROCHOT", "owner_id": 32, "owner_lun": 0, "raw_reading": 0.000000, "type": "processor", "type_number": 7, "reading": 32768.000000, "sensor_state": 0, "discrete_state": 3, "settable_readable_threshMask": 0, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "unknown" }, { "id": 26, "sensor_number": 147, "name": "CPU_THERMTRIP", "owner_id": 32, "owner_lun": 0, "raw_reading": 0.000000, "type": "processor", "type_number": 7, "reading": 32768.000000, "sensor_state": 0, "discrete_state": 111, "settable_readable_threshMask": 0, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "unknown" }, { "id": 27, "sensor_number": 153, "name": "CPU_CATERR", "owner_id": 32, "owner_lun": 0, "raw_reading": 0.000000, "type": "processor", "type_number": 7, "reading": 32768.000000, "sensor_state": 0, "discrete_state": 3, "settable_readable_threshMask": 0, "lower_non_recoverable_threshold": 0.000000, "lower_critical_threshold": 0.000000, "lower_non_critical_threshold": 0.000000, "higher_non_critical_threshold": 0.000000, "higher_critical_threshold": 0.000000, "higher_non_recoverable_threshold": 0.000000, "accessible": 0, "unit": "unknown" } ] ================================================ FILE: providers/asrockrack/helpers.go ================================================ package asrockrack import ( "bytes" "context" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/http/httputil" "os" "github.com/bmc-toolbox/bmclib/v2/constants" brrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/bmc-toolbox/common" ) // API session setup response payload type loginSession struct { CSRFToken string `json:"csrftoken,omitempty"` Privilege int `json:"privilege,omitempty"` RACSessionID int `json:"racsession_id,omitempty"` ExtendedPrivilege int `json:"extendedpriv,omitempty"` } // Firmware info endpoint response payload type firmwareInfo struct { BMCVersion string `json:"BMC_fw_version"` BIOSVersion string `json:"BIOS_fw_version"` MEVersion string `json:"ME_fw_version"` MicrocodeVersion string `json:"Micro_Code_version"` CPLDVersion string `json:"CPLD_version"` CMVersion string `json:"CM_version"` BPBVersion string `json:"BPB_version"` NodeID string `json:"Node_id"` } type biosPOSTCode struct { PostStatus int `json:"poststatus"` PostData int `json:"postdata"` } // component is part of a payload returned by the inventory info endpoint type component struct { DeviceID int `json:"device_id"` DeviceName string `json:"device_name"` DeviceType string `json:"device_type"` ProductManufacturerName string `json:"product_manufacturer_name"` ProductName string `json:"product_name"` ProductPartNumber string `json:"product_part_number"` ProductVersion string `json:"product_version"` ProductSerialNumber string `json:"product_serial_number"` ProductAssetTag string `json:"product_asset_tag"` ProductExtra string `json:"product_extra"` } // fru is part of a payload returned by the fru info endpoint type fru struct { Component string Version int `json:"version"` Length int `json:"length"` Language int `json:"language"` Manufacturer string `json:"manufacturer"` ProductName string `json:"product_name"` PartNumber string `json:"part_number"` ProductVersion string `json:"product_version"` SerialNumber string `json:"serial_number"` AssetTag string `json:"asset_tag"` FruFileID string `json:"fru_file_id"` Type string `json:"type"` CustomFields string `json:"custom_fields"` } // sensor is part of the payload returned by the sensors endpoint type sensor struct { ID int `json:"id"` SensorNumber int `json:"sensor_number"` Name string `json:"name"` OwnerID int `json:"owner_id"` OwnerLun int `json:"owner_lun"` RawReading float64 `json:"raw_reading"` Type string `json:"type"` TypeNumber int `json:"type_number"` Reading float64 `json:"reading"` SensorState int `json:"sensor_state"` DiscreteState int `json:"discrete_state"` SettableReadableThreshMask int `json:"settable_readable_threshMask"` LowerNonRecoverableThreshold float64 `json:"lower_non_recoverable_threshold"` LowerCriticalThreshold float64 `json:"lower_critical_threshold"` LowerNonCriticalThreshold float64 `json:"lower_non_critical_threshold"` HigherNonCriticalThreshold float64 `json:"higher_non_critical_threshold"` HigherCriticalThreshold float64 `json:"higher_critical_threshold"` HigherNonRecoverableThreshold float64 `json:"higher_non_recoverable_threshold"` Accessible int `json:"accessible"` Unit string `json:"unit"` } // Payload to preseve config when updating the BMC firmware type preserveConfig struct { FlashStatus int `json:"flash_status"` // 1 = full firmware flash, 2 = section based flash, 3 - version compare flash PreserveConfig int `json:"preserve_config"` PreserveNetwork int `json:"preserve_network"` PreserveUser int `json:"preserve_user"` } // Firmware flash progress // { "id": 1, "action": "Flashing...", "progress": "12% done ", "state": 0 } // { "id": 1, "action": "Flashing...", "progress": "100% done", "state": 0 } type upgradeProgress struct { ID int `json:"id,omitempty"` Action string `json:"action,omitempty"` Progress string `json:"progress,omitempty"` State int `json:"state,omitempty"` } // Chassis status struct type chassisStatus struct { PowerStatus int `json:"power_status"` LEDStatus int `json:"led_status"` } // BIOS upgrade commands // 2 == configure // 3 == apply upgrade type biosUpdateAction struct { Action int `json:"action"` } var ( knownPOSTCodes = map[int]string{ 160: constants.POSTStateOS, 2: constants.POSTStateBootINIT, // no differentiation between BIOS init and PXE boot 144: constants.POSTStateUEFI, 154: constants.POSTStateUEFI, 178: constants.POSTStateUEFI, } ) func (a *ASRockRack) listUsers(ctx context.Context) ([]*UserAccount, error) { resp, statusCode, err := a.queryHTTPS(ctx, "api/settings/users", "GET", nil, nil, 0) if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("non 200 response: %d", statusCode) } accounts := []*UserAccount{} err = json.Unmarshal(resp, &accounts) if err != nil { return nil, err } return accounts, nil } func (a *ASRockRack) createUpdateUser(ctx context.Context, account *UserAccount) error { endpoint := "api/settings/users/" + fmt.Sprintf("%d", account.ID) payload, err := json.Marshal(account) if err != nil { return err } headers := map[string]string{"Content-Type": "application/json"} _, statusCode, err := a.queryHTTPS(ctx, endpoint, "PUT", bytes.NewReader(payload), headers, 0) if err != nil { return err } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d", statusCode) } return nil } // 1 Set BMC to flash mode and prepare flash area // // with the BMC set in flash mode, no new logins are accepted // and only a few endpoints can be queried with the existing session // one of the few being the install progress/flash status endpoint. func (a *ASRockRack) setFlashMode(ctx context.Context) error { device := common.NewDevice() device.Metadata = map[string]string{} _ = a.fruAttributes(ctx, &device) pConfig := &preserveConfig{} // preserve config is needed by e3c256d4i switch device.Model { case E3C256D4ID_NL: pConfig = &preserveConfig{PreserveConfig: 1} } payload, err := json.Marshal(pConfig) if err != nil { return err } headers := map[string]string{"Content-Type": "application/json"} _, statusCode, err := a.queryHTTPS(ctx, "api/maintenance/flash", "PUT", bytes.NewReader(payload), headers, 0) if err != nil { return err } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d", statusCode) } a.resetRequired = true return nil } func multipartSize(fieldname, filename string) int64 { body := &bytes.Buffer{} form := multipart.NewWriter(body) _, _ = form.CreateFormFile(fieldname, filename) _ = form.Close() return int64(body.Len()) } // 2 Upload the firmware file func (a *ASRockRack) uploadFirmware(ctx context.Context, endpoint string, file *os.File) error { var size int64 finfo, err := file.Stat() if err != nil { return fmt.Errorf("unable to determine file size: %w", err) } size = finfo.Size() fieldName, fileName := "fwimage", "image" contentLength := multipartSize(fieldName, fileName) + size // Before reading the file, rewind to the beginning _, _ = file.Seek(0, 0) // setup pipe pipeReader, pipeWriter := io.Pipe() defer pipeReader.Close() // initiate a mulitpart writer form := multipart.NewWriter(pipeWriter) errCh := make(chan error, 1) go func() { defer pipeWriter.Close() // create form part part, err := form.CreateFormFile(fieldName, fileName) if err != nil { errCh <- err return } // copy from source into form part writer _, err = io.Copy(part, file) if err != nil { errCh <- err return } // add terminating boundary to multipart form errCh <- form.Close() }() // multi-part content type headers := map[string]string{ "Content-Type": form.FormDataContentType(), } // POST payload _, statusCode, err := a.queryHTTPS(ctx, endpoint, "POST", pipeReader, headers, contentLength) if err != nil { return err } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d", statusCode) } return nil } // 3. Verify uploaded firmware file - to be invoked after uploadFirmware() func (a *ASRockRack) verifyUploadedFirmware(ctx context.Context) error { _, statusCode, err := a.queryHTTPS(ctx, "api/maintenance/firmware/verification", "GET", nil, nil, 0) if err != nil { return err } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d", statusCode) } return nil } // 4. Start firmware flashing process - to be invoked after verifyUploadedFirmware func (a *ASRockRack) upgradeBMC(ctx context.Context) error { endpoint := "api/maintenance/firmware/upgrade" // preserve all configuration during upgrade, full flash pConfig := &preserveConfig{FlashStatus: 1, PreserveConfig: 1, PreserveNetwork: 1, PreserveUser: 1} payload, err := json.Marshal(pConfig) if err != nil { return err } headers := map[string]string{"Content-Type": "application/json"} _, statusCode, err := a.queryHTTPS(ctx, endpoint, "PUT", bytes.NewReader(payload), headers, 0) if err != nil { return err } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d", statusCode) } return nil } // 5. firmware flash progress func (a *ASRockRack) flashProgress(ctx context.Context, endpoint string) (*upgradeProgress, error) { resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "GET", nil, nil, 0) if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("non 200 response: %d", statusCode) } p := &upgradeProgress{} err = json.Unmarshal(resp, p) if err != nil { return nil, err } return p, nil } // Query firmware information from the BMC func (a *ASRockRack) firmwareInfo(ctx context.Context) (*firmwareInfo, error) { resp, statusCode, err := a.queryHTTPS(ctx, "api/asrr/fw-info", "GET", nil, nil, 0) if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("non 200 response: %d", statusCode) } f := &firmwareInfo{} err = json.Unmarshal(resp, f) if err != nil { return nil, err } return f, nil } // Query BIOS/UEFI POST code information from the BMC func (a *ASRockRack) postCodeInfo(ctx context.Context) (*biosPOSTCode, error) { resp, statusCode, err := a.queryHTTPS(ctx, "/api/asrr/getbioscode", "GET", nil, nil, 0) if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("non 200 response: %d", statusCode) } b := &biosPOSTCode{} err = json.Unmarshal(resp, b) if err != nil { return nil, err } return b, nil } // Query the inventory info endpoint func (a *ASRockRack) inventoryInfoE3C246D41D(ctx context.Context) ([]*component, error) { resp, statusCode, err := a.queryHTTPS(ctx, "api/asrr/inventory_info", "GET", nil, nil, 0) if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("non 200 response: %d", statusCode) } components := []*component{} err = json.Unmarshal(resp, &components) if err != nil { return nil, err } return components, nil } // Query the fru info endpoint func (a *ASRockRack) fruInfo(ctx context.Context) ([]*fru, error) { resp, statusCode, err := a.queryHTTPS(ctx, "api/fru", "GET", nil, nil, 0) if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("non 200 response: %d", statusCode) } data := []map[string]*fru{} err = json.Unmarshal(resp, &data) if err != nil { return nil, err } if len(data) == 0 { return nil, fmt.Errorf("no FRU data returned") } frus := []*fru{} for key, f := range data[0] { switch key { case "chassis", "board", "product": frus = append(frus, &fru{ Component: key, Version: f.Version, Length: f.Length, Language: f.Language, Manufacturer: f.Manufacturer, ProductName: f.ProductName, PartNumber: f.PartNumber, ProductVersion: f.ProductVersion, SerialNumber: f.SerialNumber, AssetTag: f.SerialNumber, FruFileID: f.FruFileID, CustomFields: f.CustomFields, Type: f.Type, }) } } return frus, nil } // Query the sensors endpoint func (a *ASRockRack) sensors(ctx context.Context) ([]*sensor, error) { resp, statusCode, err := a.queryHTTPS(ctx, "api/sensors", "GET", nil, nil, 0) if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("non 200 response: %d", statusCode) } sensors := []*sensor{} err = json.Unmarshal(resp, &sensors) if err != nil { return nil, err } return sensors, nil } // Set the BIOS upgrade configuration // - preserve current configuration func (a *ASRockRack) biosUpgradeConfiguration(ctx context.Context) error { endpoint := "api/asrr/maintenance/BIOS/configuration" // Preserve existing configuration? p := biosUpdateAction{Action: 2} payload, err := json.Marshal(p) if err != nil { return err } headers := map[string]string{"Content-Type": "application/json"} resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "POST", bytes.NewReader(payload), headers, 0) if err != nil { return err } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d", statusCode) } f := &firmwareInfo{} return json.Unmarshal(resp, f) } // Run BIOS upgrade func (a *ASRockRack) upgradeBIOS(ctx context.Context) error { endpoint := "api/asrr/maintenance/BIOS/upgrade" // Run upgrade p := biosUpdateAction{Action: 3} payload, err := json.Marshal(p) if err != nil { return err } headers := map[string]string{"Content-Type": "application/json"} resp, statusCode, err := a.queryHTTPS(ctx, endpoint, "POST", bytes.NewReader(payload), headers, 0) if err != nil { return err } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d", statusCode) } f := &firmwareInfo{} return json.Unmarshal(resp, f) } // Returns the chassis status object which includes the power state func (a *ASRockRack) chassisStatusInfo(ctx context.Context) (*chassisStatus, error) { resp, statusCode, err := a.queryHTTPS(ctx, "/api/chassis-status", "GET", nil, nil, 0) if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("non 200 response: %d", statusCode) } chassisStatus := chassisStatus{} err = json.Unmarshal(resp, &chassisStatus) if err != nil { return nil, err } return &chassisStatus, nil } // Aquires a session id cookie and a csrf token func (a *ASRockRack) httpsLogin(ctx context.Context) error { urlEndpoint := "api/session" // login payload payload := []byte( fmt.Sprintf("username=%s&password=%s&certlogin=0", a.username, a.password, ), ) headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} resp, statusCode, err := a.queryHTTPS(ctx, urlEndpoint, "POST", bytes.NewReader(payload), headers, 0) if err != nil { return fmt.Errorf("logging in: %w", err) } if statusCode == 401 { return brrs.ErrLoginFailed } // Unmarshal login session err = json.Unmarshal(resp, a.loginSession) if err != nil { return fmt.Errorf("unmarshalling response payload: %w", err) } return nil } // Close ends the BMC session func (a *ASRockRack) httpsLogout(ctx context.Context) error { _, statusCode, err := a.queryHTTPS(ctx, "api/session", "DELETE", nil, nil, 0) if err != nil { return fmt.Errorf("logging out: %w", err) } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response at https logout: %d", statusCode) } return nil } // queryHTTPS run the HTTPS query passing in the required headers // the / suffix should be excluded from the URLendpoint // returns - response body, http status code, error if any func (a *ASRockRack) queryHTTPS(ctx context.Context, endpoint, method string, payload io.Reader, headers map[string]string, contentLength int64) ([]byte, int, error) { var body []byte var err error var req *http.Request URL := fmt.Sprintf("https://%s/%s", a.ip, endpoint) req, err = http.NewRequestWithContext(ctx, method, URL, payload) if err != nil { return nil, 0, err } // add headers req.Header.Add("X-CSRFTOKEN", a.loginSession.CSRFToken) for k, v := range headers { req.Header.Add(k, v) } // Content-Length headers are ignored, unless defined in this manner // https://go.googlesource.com/go/+/go1.16/src/net/http/request.go#161 // https://go.googlesource.com/go/+/go1.16/src/net/http/request.go#88 if contentLength > 0 { req.ContentLength = contentLength } // debug dump request if os.Getenv(constants.EnvEnableDebug) == "true" { reqDump, _ := httputil.DumpRequestOut(req, true) a.log.V(3).Info("trace", "url", URL, "requestDump", string(reqDump)) } resp, err := a.httpClient.Do(req) if err != nil { return body, 0, err } // debug dump response if os.Getenv(constants.EnvEnableDebug) == "true" { respDump, _ := httputil.DumpResponse(resp, true) a.log.V(3).Info("trace", "responseDump", string(respDump)) } body, err = io.ReadAll(resp.Body) if err != nil { return body, 0, err } defer resp.Body.Close() return body, resp.StatusCode, nil } ================================================ FILE: providers/asrockrack/helpers_test.go ================================================ package asrockrack import ( "context" "testing" "gopkg.in/go-playground/assert.v1" ) func Test_FirmwareInfo(t *testing.T) { expected := firmwareInfo{ BMCVersion: "0.01.00", BIOSVersion: "L2.07B", MEVersion: "5.1.3.78", MicrocodeVersion: "000000ca", CPLDVersion: "N/A", CMVersion: "0.13.01", BPBVersion: "0.0.002.0", NodeID: "2", } err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } fwInfo, err := aClient.firmwareInfo(context.TODO()) if err != nil { t.Errorf("firmwareInfo: %s", err.Error()) } assert.Equal(t, expected, fwInfo) } func TestInventoryInfo(t *testing.T) { err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } inventory, err := aClient.inventoryInfoE3C246D41D(context.TODO()) if err != nil { t.Fatal(err.Error()) } assert.Equal(t, 6, len(inventory)) assert.Equal(t, "CPU", inventory[0].DeviceType) assert.Equal(t, "Storage device", inventory[5].DeviceType) } func Test_fruInfo(t *testing.T) { err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } frus, err := aClient.fruInfo(context.TODO()) if err != nil { t.Fatal(err.Error()) } assert.Equal(t, 3, len(frus)) } func Test_sensors(t *testing.T) { err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } sensors, err := aClient.sensors(context.TODO()) if err != nil { t.Fatal(err.Error()) } assert.Equal(t, 27, len(sensors)) } func Test_biosPOSTCode(t *testing.T) { expected := biosPOSTCode{ PostStatus: 1, PostData: 160, } err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } info, err := aClient.postCodeInfo(context.TODO()) if err != nil { t.Error(err.Error()) } assert.Equal(t, expected, info) } func Test_chassisStatus(t *testing.T) { expected := chassisStatus{ PowerStatus: 1, LEDStatus: 0, } err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } info, err := aClient.chassisStatusInfo(context.TODO()) if err != nil { t.Error(err.Error()) } assert.Equal(t, expected, info) } ================================================ FILE: providers/asrockrack/inventory.go ================================================ package asrockrack import ( "context" "github.com/bmc-toolbox/common" ) // Inventory returns hardware and firmware inventory func (a *ASRockRack) Inventory(ctx context.Context) (device *common.Device, err error) { // initialize device to be populated with inventory newDevice := common.NewDevice() device = &newDevice device.Status = &common.Status{} device.Metadata = map[string]string{} // populate device BMC, BIOS component attributes err = a.fruAttributes(ctx, device) if err != nil { return nil, err } // populate device System components attributes err = a.systemAttributes(ctx, device) if err != nil { return nil, err } // populate device health based on sensor readings // // sensor data collection can fail for a myriad of reasons // we log the error and keep going err = a.systemHealth(ctx, device) if err != nil { a.log.V(2).Error(err, "sensor data collection error", "deviceModel", a.deviceModel) } return device, nil } // systemHealth collects system health information based on the sensors data func (a *ASRockRack) systemHealth(ctx context.Context, device *common.Device) error { sensors, err := a.sensors(ctx) if err != nil { return err } ok := true device.Status.Health = "OK" for _, sensor := range sensors { switch sensor.Name { case "CPU_CATERR", "CPU_THERMTRIP", "CPU_PROCHOT": if sensor.SensorState != 0 { ok = false device.Status.State = sensor.Name break } default: if sensor.SensorState != 1 { ok = false device.Status.State = sensor.Name break } } } if !ok { device.Status.Health = "CRITICAL" } // we don't want to fail inventory collection hence ignore POST code collection error device.Status.PostCodeStatus, device.Status.PostCode, _ = a.PostCode(ctx) return nil } // fruAttributes collects chassis information func (a *ASRockRack) fruAttributes(ctx context.Context, device *common.Device) error { components, err := a.fruInfo(ctx) if err != nil { return err } for _, component := range components { switch component.Component { case "board": device.Vendor = component.Manufacturer device.Model = component.ProductName device.Serial = component.SerialNumber case "chassis": device.Enclosures = append(device.Enclosures, &common.Enclosure{ Common: common.Common{ Serial: component.SerialNumber, Description: component.Type, }, }) case "product": device.Metadata["product.manufacturer"] = component.Manufacturer device.Metadata["product.name"] = component.ProductName device.Metadata["product.part_number"] = component.PartNumber device.Metadata["product.version"] = component.ProductVersion device.Metadata["product.serialnumber"] = component.SerialNumber } } return nil } // systemAttributes collects system component attributes func (a *ASRockRack) systemAttributes(ctx context.Context, device *common.Device) error { fwInfo, err := a.firmwareInfo(ctx) if err != nil { return err } device.BIOS = &common.BIOS{ Common: common.Common{ Vendor: device.Vendor, Model: device.Model, Firmware: &common.Firmware{Installed: fwInfo.BIOSVersion}, }, } device.BMC = &common.BMC{ Common: common.Common{ Vendor: device.Vendor, Model: device.Model, Firmware: &common.Firmware{Installed: fwInfo.BMCVersion}, }, } if fwInfo.CPLDVersion != "N/A" { device.CPLDs = append(device.CPLDs, &common.CPLD{ Common: common.Common{ Vendor: device.Vendor, Model: device.Model, Firmware: &common.Firmware{Installed: fwInfo.CPLDVersion}, }, }) } device.Metadata["node_id"] = fwInfo.NodeID switch device.Model { case E3C246D4ID_NL, E3C246D4I_NL: return a.componentAttributesE3C246(ctx, fwInfo, device) default: return nil } } func (a *ASRockRack) componentAttributesE3C246(ctx context.Context, fwInfo *firmwareInfo, device *common.Device) error { // TODO: implement newer device inventory components, err := a.inventoryInfoE3C246D41D(ctx) if err != nil { return err } for _, component := range components { switch component.DeviceType { case "CPU": device.CPUs = append(device.CPUs, &common.CPU{ Common: common.Common{ Vendor: component.ProductManufacturerName, Model: component.ProductName, Firmware: &common.Firmware{ Installed: fwInfo.MicrocodeVersion, Metadata: map[string]string{ "Intel_ME_version": fwInfo.MEVersion, }, }, }, }, ) case "Memory": device.Memory = append(device.Memory, &common.Memory{ Common: common.Common{ Vendor: component.ProductManufacturerName, Serial: component.ProductSerialNumber, Description: component.ProductExtra, }, PartNumber: component.ProductPartNumber, Type: component.DeviceName, }, ) case "Storage device": var vendor string if component.ProductManufacturerName == "N/A" && component.ProductPartNumber != "N/A" { vendor = common.FormatVendorName(component.ProductPartNumber) } device.Drives = append(device.Drives, &common.Drive{ Common: common.Common{ Vendor: vendor, Serial: component.ProductSerialNumber, ProductName: component.ProductPartNumber, }, }, ) } } return nil } ================================================ FILE: providers/asrockrack/inventory_test.go ================================================ package asrockrack import ( "context" "testing" "github.com/stretchr/testify/assert" ) func TestGetInventory(t *testing.T) { device, err := aClient.Inventory(context.TODO()) if err != nil { t.Fatal(err) } aClient.deviceModel = E3C246D4I_NL assert.NotNil(t, device) assert.Equal(t, "ASRockRack", device.Vendor) assert.Equal(t, E3C246D4I_NL, device.Model) assert.Equal(t, "L2.07B", device.BIOS.Firmware.Installed) assert.Equal(t, "0.01.00", device.BMC.Firmware.Installed) assert.Equal(t, "000000ca", device.CPUs[0].Firmware.Installed) assert.Equal(t, "Intel(R) Xeon(R) E-2278G CPU @ 3.40GHz", device.CPUs[0].Model) assert.Equal(t, 2, len(device.Memory)) assert.Equal(t, 2, len(device.Drives)) assert.Equal(t, "OK", device.Status.Health) } ================================================ FILE: providers/asrockrack/mock_test.go ================================================ package asrockrack import ( "bytes" "encoding/json" "io" "log" "net/http" "net/http/httptest" "net/url" "os" "strconv" "strings" "testing" "github.com/bombsimon/logrusr/v2" "github.com/sirupsen/logrus" ) var ( loginPayload = []byte(`username=foo&password=bar&certlogin=0`) loginResponse = []byte(`{ "ok": 0, "privilege": 4, "extendedpriv": 259, "racsession_id": 10, "remote_addr": "136.144.50.145", "server_name": "10.230.148.171", "server_addr": "10.230.148.171", "HTTPSEnabled": 1, "CSRFToken": "l5L29IP7" }`) fwinfoResponse = []byte(`{ "BMC_fw_version": "0.01.00", "BIOS_fw_version": "L2.07B", "ME_fw_version": "5.1.3.78", "Micro_Code_version": "000000ca", "CPLD_version": "N\/A", "CM_version": "0.13.01", "BPB_version": "0.0.002.0", "Node_id": "2" }`) fwUploadResponse = []byte(`{"cc": 0}`) fwVerificationResponse = []byte(`[ { "id": 1, "current_image_name": "ast2500e", "current_image_version1": "0.01.00", "current_image_version2": "", "new_image_version": "0.03.00", "section_status": 0, "verification_status": 5 } ]`) fwUpgradeProgress = []byte(`{ "id": 1, "action": "Flashing...", "progress": "__PERCENT__% done ", "state": __STATE__ }`) usersPayload = []byte(`[ { "id": 1, "name": "anonymous", "access": 0, "kvm": 1, "vmedia": 1, "snmp": 0, "prev_snmp": 0, "network_privilege": "administrator", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "none", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "ami_format", "ssh_key": "Not Available", "creation_time": 4802 }, { "id": 2, "name": "admin", "access": 1, "kvm": 1, "vmedia": 1, "snmp": 0, "prev_snmp": 0, "network_privilege": "administrator", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "none", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "ami_format", "ssh_key": "Not Available", "creation_time": 188 }, { "id": 3, "name": "foo", "access": 1, "kvm": 1, "vmedia": 1, "snmp": 0, "prev_snmp": 0, "network_privilege": "administrator", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "none", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "ami_format", "ssh_key": "Not Available", "creation_time": 4802 }, { "id": 4, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 5, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 6, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 7, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 8, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 9, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 }, { "id": 10, "name": "", "access": 0, "kvm": 0, "vmedia": 0, "snmp": 0, "prev_snmp": 0, "network_privilege": "", "fixed_user_count": 2, "snmp_access": "", "OEMProprietary_level_Privilege": 1, "privilege_limit_serial": "", "snmp_authentication_protocol": "", "snmp_privacy_protocol": "", "email_id": "", "email_format": "", "ssh_key": "Not Available", "creation_time": 0 } ]`) inventoryinfoResponse = []byte(`[ { "device_id": 1, "device_name": "CPU1", "device_type": "CPU", "product_manufacturer_name": "Intel(R) Corporation", "product_name": "Intel(R) Xeon(R) E-2278G CPU @ 3.40GHz", "product_part_number": "N\/A", "product_version": "N\/A", "product_serial_number": "N\/A", "product_asset_tag": "N\/A", "product_extra": "N\/A" }, { "device_id": 5, "device_name": "DDR4_A1", "device_type": "Memory", "product_manufacturer_name": "Micron", "product_name": "SODIMM", "product_part_number": "18ASF2G72HZ-2G6E1 ", "product_version": "N\/A", "product_serial_number": "2724B52D", "product_asset_tag": "N\/A", "product_extra": "2666 MT\/s 16GB" }, { "device_id": 7, "device_name": "DDR4_B1", "device_type": "Memory", "product_manufacturer_name": "Micron", "product_name": "SODIMM", "product_part_number": "18ASF2G72HZ-2G6E1 ", "product_version": "N\/A", "product_serial_number": "2724B58A", "product_asset_tag": "N\/A", "product_extra": "2666 MT\/s 16GB" }, { "device_id": 37, "device_name": "PCIe card 1", "device_type": "PCIe & OCP Card", "product_manufacturer_name": "8086(Intel Corporation)", "product_name": "020000(Ethernet controller)", "product_part_number": "1572", "product_version": "N\/A", "product_serial_number": "N\/A", "product_asset_tag": "PCIE7", "product_extra": "N\/A" }, { "device_id": 105, "device_name": "Storage ", "device_type": "Storage device", "product_manufacturer_name": "N\/A", "product_name": "N\/A", "product_part_number": "INTEL SSDSC2KB480G8", "product_version": "N\/A", "product_serial_number": "PHYF001303ED480BGN", "product_asset_tag": "SATA_4", "product_extra": "N\/A" }, { "device_id": 106, "device_name": "Storage ", "device_type": "Storage device", "product_manufacturer_name": "N\/A", "product_name": "N\/A", "product_part_number": "INTEL SSDSC2KB480G8", "product_version": "N\/A", "product_serial_number": "BTYF01940L38480BGN", "product_asset_tag": "SATA_5", "product_extra": "N\/A" } ]`) fruinfoResponse = []byte(`[ { "device": { "id": 0, "name": "BMC_FRU" }, "common_header": { "version": 1, "internal_use_area_start_offset": 0, "chassis_info_area_start_offset": 1, "board_info_area_start_offset": 4, "product_info_area_start_offset": 11, "multi_record_area_start_offset": 0 }, "chassis": { "version": 1, "length": 3, "type": "Main Server Chassis", "part_number": "", "serial_number": "K61206147700263", "custom_fields": "" }, "board": { "version": 1, "length": 7, "language": 0, "date": "Mon Jul 20 06:04:00 2020\\n", "manufacturer": "ASRockRack", "product_name": "E3C246D4I-NL", "serial_number": "197965920000514", "part_number": "", "fru_file_id": "", "custom_fields": "" }, "product": { "version": 1, "length": 7, "language": 0, "manufacturer": "Packet", "product_name": "c3.small.x86", "part_number": "Open19", "product_version": "R1.00", "serial_number": "D6S0R8000736", "asset_tag": "", "fru_file_id": "", "custom_fields": "" } } ]`) biosPOSTCodeResponse = []byte(`{ "poststatus": 1, "postdata": 160 }`) chassisStatusResponse = []byte(`{ "power_status": 1, "led_status": 0 }`) // TODO: implement under rw mutex httpRequestTestVar *http.Request ) // setup test BMC var server *httptest.Server var bmcURL *url.URL var fwUpgradeState *testFwUpgradeState type testFwUpgradeState struct { FlashModeSet bool FirmwareUploaded bool FirmwareVerified bool UpgradeInitiated bool UpgradePercent int ResetDone bool } // the bmc lib client var aClient *ASRockRack func TestMain(m *testing.M) { // setup mock server server = mockASRockBMC() bmcURL, _ = url.Parse(server.URL) l := logrus.New() l.Level = logrus.DebugLevel // setup bmc client tLog := logrusr.New(l) aClient = New(bmcURL.Host, "foo", "bar", tLog) // firmware update test state fwUpgradeState = &testFwUpgradeState{} os.Exit(m.Run()) } // ///////////// mock bmc service /////////////////////////// func mockASRockBMC() *httptest.Server { handler := http.NewServeMux() handler.HandleFunc("/", index) handler.HandleFunc("/api/session", session) handler.HandleFunc("/api/asrr/fw-info", fwinfo) handler.HandleFunc("/api/fru", fruinfo) handler.HandleFunc("/api/asrr/inventory_info", inventoryinfo) handler.HandleFunc("/api/sensors", sensorsinfo) handler.HandleFunc("/api/asrr/getbioscode", biosPOSTCodeinfo) handler.HandleFunc("/api/chassis-status", chassisStatusInfo) // fw update endpoints - in order of invocation handler.HandleFunc("/api/maintenance/flash", bmcFirmwareUpgrade) handler.HandleFunc("/api/maintenance/firmware", bmcFirmwareUpgrade) handler.HandleFunc("/api/maintenance/firmware/firmware", bmcFirmwareUpgrade) handler.HandleFunc("/api/maintenance/firmware/verification", bmcFirmwareUpgrade) handler.HandleFunc("/api/maintenance/firmware/upgrade", bmcFirmwareUpgrade) handler.HandleFunc("/api/maintenance/firmware/flash-progress", bmcFirmwareUpgrade) handler.HandleFunc("/api/maintenance/reset", bmcFirmwareUpgrade) handler.HandleFunc("/api/asrr/maintenance/BIOS/firmware", biosFirmwareUpgrade) // user accounts endpoints handler.HandleFunc("/api/settings/users", userAccountList) handler.HandleFunc("/api/settings/users/3", userAccountList) return httptest.NewTLSServer(handler) } func index(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": _, _ = w.Write([]byte(`ASRockRack`)) } } func userAccountList(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": if os.Getenv("TEST_FAIL_QUERY") != "" { w.WriteHeader(http.StatusInternalServerError) } else { _, _ = w.Write(usersPayload) } case "PUT": httpRequestTestVar = r } } func biosFirmwareUpgrade(w http.ResponseWriter, r *http.Request) { switch r.Method { case "POST": switch r.RequestURI { case "/api/asrr/maintenance/BIOS/firmware": // validate content type if !strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") { w.WriteHeader(http.StatusBadRequest) } // parse multipart form err := r.ParseMultipartForm(100) if err != nil { w.WriteHeader(http.StatusBadRequest) } } } } func bmcFirmwareUpgrade(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": switch r.RequestURI { // 3. bmc verifies uploaded firmware image case "/api/maintenance/firmware/verification": if !fwUpgradeState.FirmwareUploaded { w.WriteHeader(http.StatusBadRequest) } fwUpgradeState.FirmwareVerified = true _, _ = w.Write(fwVerificationResponse) // 5. flash progress case "/api/maintenance/firmware/flash-progress": if !fwUpgradeState.UpgradeInitiated { w.WriteHeader(http.StatusBadRequest) } resp := fwUpgradeProgress if fwUpgradeState.UpgradePercent >= 100 { fwUpgradeState.UpgradePercent = 100 // state: 2 indicates firmware flash complete resp = bytes.Replace(resp, []byte("__STATE__"), []byte(strconv.Itoa(2)), 1) } else { // state: 0 indicates firmware flash in progress resp = bytes.Replace(resp, []byte("__STATE__"), []byte(strconv.Itoa(0)), 1) fwUpgradeState.UpgradePercent += 50 } resp = bytes.Replace(resp, []byte("__PERCENT__"), []byte(strconv.Itoa(fwUpgradeState.UpgradePercent)), 1) _, _ = w.Write(resp) } case "PUT": switch r.RequestURI { // 1. set device to flash mode case "/api/maintenance/flash": fwUpgradeState.FlashModeSet = true w.WriteHeader(http.StatusOK) // 4. run the upgrade case "/api/maintenance/firmware/upgrade": if !fwUpgradeState.FirmwareVerified { w.WriteHeader(http.StatusBadRequest) } if r.Header.Get("Content-Type") != "application/json" { w.WriteHeader(http.StatusBadRequest) } p := &preserveConfig{} err := json.NewDecoder(r.Body).Decode(&p) if err != nil { w.WriteHeader(http.StatusBadRequest) return } // config should be preserved if p.PreserveConfig != 1 { w.WriteHeader(http.StatusBadRequest) } // full firmware flash if p.FlashStatus != 1 { w.WriteHeader(http.StatusBadRequest) } fwUpgradeState.UpgradeInitiated = true // respond with request body b := new(bytes.Buffer) _, _ = b.ReadFrom(r.Body) _, _ = w.Write(b.Bytes()) } case "POST": switch r.RequestURI { case "/api/maintenance/reset": w.WriteHeader(http.StatusOK) // 2. upload firmware case "/api/maintenance/firmware", "/api/maintenance/firmware/firmware": // validate flash mode set if !fwUpgradeState.FlashModeSet { w.WriteHeader(http.StatusBadRequest) } // validate content type if !strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") { w.WriteHeader(http.StatusBadRequest) } // parse multipart form err := r.ParseMultipartForm(100) if err != nil { w.WriteHeader(http.StatusBadRequest) } fwUpgradeState.FirmwareUploaded = true _, _ = w.Write(fwUploadResponse) } default: w.WriteHeader(http.StatusBadRequest) } } func fwinfo(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": _, _ = w.Write(fwinfoResponse) } } func fruinfo(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": _, _ = w.Write(fruinfoResponse) } } func inventoryinfo(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": _, _ = w.Write(inventoryinfoResponse) } } func sensorsinfo(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": fh, err := os.Open("./fixtures/E3C246D4I-NL/sensors.json") if err != nil { log.Fatal(err) } b, err := io.ReadAll(fh) if err != nil { log.Fatal(err) } _, _ = w.Write(b) } } func biosPOSTCodeinfo(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": _, _ = w.Write(biosPOSTCodeResponse) } } func chassisStatusInfo(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": _, _ = w.Write(chassisStatusResponse) } } func session(w http.ResponseWriter, r *http.Request) { switch r.Method { case "POST": // login to BMC b, _ := io.ReadAll(r.Body) if string(b) == string(loginPayload) { // login request needs to be of the right content-typ if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { w.WriteHeader(http.StatusBadRequest) } w.Header().Set("Content-Type", "application/json") http.SetCookie(w, &http.Cookie{Name: "QSESSIONID", Value: "94ed00f482249dd77arIcp6eBBJaik", Path: "/"}) _, _ = w.Write(loginResponse) } else { w.WriteHeader(http.StatusBadRequest) } case "DELETE": if r.Header.Get("X-Csrftoken") != "l5L29IP7" { w.WriteHeader(http.StatusBadRequest) } } } ================================================ FILE: providers/asrockrack/power.go ================================================ package asrockrack import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strings" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" ) type power struct { Command int `json:"power_command"` } // PowerStateGet gets the power state of a machine func (a *ASRockRack) PowerStateGet(ctx context.Context) (state string, err error) { info, err := a.chassisStatusInfo(ctx) if err != nil { if strings.Contains(err.Error(), "401") { // during a BMC update, only the flash-progress endpoint can be queried // and so we cannot determine server power status // we don't return an error here because we don't want the bmclib client to retry another provider. progress, err := a.flashProgress(ctx, "/api/maintenance/firmware/flash-progress") if err == nil && progress.Action != "" { a.log.V(2).WithValues( "action", progress.Action, "progress", progress.Progress, "state", progress.State, ).Info("bmc in flash mode, power status cannot be determined") return "", errors.Wrap( bmclibErrs.ErrBMCUpdating, fmt.Sprintf( "action: %s, progress: %s, state: %d", progress.Action, progress.Progress, progress.State, ), ) } } return "", errors.Wrap(bmclibErrs.ErrPowerStatusRead, err.Error()) } switch info.PowerStatus { case 0: return "Off", nil case 1: return "On", nil default: return "", errors.Wrap( bmclibErrs.ErrPowerStatusRead, fmt.Errorf("unknown status: %d", info.PowerStatus).Error(), ) } } // PowerSet sets the hardware power state of a machine func (a *ASRockRack) PowerSet(ctx context.Context, state string) (ok bool, err error) { switch strings.ToLower(state) { case "on": return a.powerAction(ctx, 1) case "off": return a.powerAction(ctx, 0) case "soft": return a.powerAction(ctx, 5) case "reset": return a.powerAction(ctx, 3) case "cycle": return a.powerAction(ctx, 2) default: return false, errors.New("requested power state unknown: " + state) } } func (a *ASRockRack) powerAction(ctx context.Context, action int) (ok bool, err error) { endpoint := "/api/actions/power" p := power{Command: action} payload, err := json.Marshal(p) if err != nil { return false, err } headers := map[string]string{"Content-Type": "application/json"} _, statusCode, err := a.queryHTTPS( ctx, endpoint, "POST", bytes.NewReader(payload), headers, 0, ) if err != nil { return false, errors.Wrap(bmclibErrs.ErrPowerStatusSet, err.Error()) } if statusCode != http.StatusOK { return false, errors.Wrap( bmclibErrs.ErrNon200Response, fmt.Errorf("%d", statusCode).Error(), ) } return true, nil } // BmcReset will reset the BMC - ASRR BMCs only support a cold reset. func (a *ASRockRack) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { err = a.resetBMC(ctx) if err != nil { return false, err } return true, nil } // 4. reset BMC - performs a cold reset func (a *ASRockRack) resetBMC(ctx context.Context) error { endpoint := "api/maintenance/reset" _, statusCode, err := a.queryHTTPS(ctx, endpoint, "POST", nil, nil, 0) if err != nil { return err } // The E3C256D4ID BMC returns a 500 status error on the BMC reset request if statusCode != http.StatusOK && statusCode != http.StatusInternalServerError { return fmt.Errorf("non 200 response: %d", statusCode) } return nil } ================================================ FILE: providers/asrockrack/user.go ================================================ package asrockrack import ( "context" "fmt" "strings" "github.com/pkg/errors" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/bmc-toolbox/bmclib/v2/internal" ) var ( // TODO: standardize these across Redfish, IPMI, Vendor GUI validRoles = []string{"Administrator", "Operator", "User"} ) // UserAccount is a ASRR BMC user account struct type UserAccount struct { ID int `json:"id"` Name string `json:"name"` Access int `json:"access"` AccessByChannel string `json:"accessByChannel,omitempty"` Kvm int `json:"kvm"` Vmedia int `json:"vmedia"` NetworkPrivilege string `json:"network_privilege"` FixedUserCount int `json:"fixed_user_count"` OEMProprietaryLevelPrivilege int `json:"OEMProprietary_level_Privilege"` Privilege string `json:"privilege,omitempty"` PrivilegeByChannel string `json:"privilegeByChannel,omitempty"` PrivilegeLimitSerial string `json:"privilege_limit_serial"` SSHKey string `json:"ssh_key"` CreationTime int `json:"creation_time"` Changepassword int `json:"changepassword"` UserOperation int `json:"UserOperation"` Password string `json:"password"` ConfirmPassword string `json:"confirm_password"` PasswordSize string `json:"password_size"` PrevSNMP int `json:"prev_snmp"` SNMP int `json:"snmp"` SNMPAccess string `json:"snmp_access"` SNMPAuthenticationProtocol string `json:"snmp_authentication_protocol"` EmailFormat string `json:"email_format"` EmailID string `json:"email_id"` } // UserRead returns a list of enabled user accounts func (a *ASRockRack) UserRead(ctx context.Context) (users []map[string]string, err error) { err = a.Open(ctx) if err != nil { return nil, err } accounts, err := a.listUsers(ctx) if err != nil { return nil, errors.Wrap(bmclibErrs.ErrRetrievingUserAccounts, err.Error()) } users = make([]map[string]string, 0) for _, account := range accounts { if account.Access == 1 { user := map[string]string{ "ID": fmt.Sprintf("%d", account.ID), "Name": account.Name, "RoleID": account.NetworkPrivilege, } users = append(users, user) } } return users, nil } // UserCreate adds a new user account func (a *ASRockRack) UserCreate(ctx context.Context, user, pass, role string) (ok bool, err error) { if !internal.StringInSlice(role, validRoles) { return false, bmclibErrs.ErrInvalidUserRole } if user == "" || pass == "" || role == "" { return false, bmclibErrs.ErrUserParamsRequired } // fetch current list of accounts accounts, err := a.listUsers(ctx) if err != nil { return false, errors.Wrap(bmclibErrs.ErrRetrievingUserAccounts, err.Error()) } // identify account slot not in use for _, account := range accounts { // ASRR BMCs have a reserved slot 1 for a disabled Anonymous, no idea why. if account.ID == 1 { continue } account := account if account.Name == user { return false, errors.Wrap(bmclibErrs.ErrUserAccountExists, user) } if account.Access == 0 && account.Name == "" { newAccount := newUserAccount(account.ID, user, pass, strings.ToLower(role)) err := a.createUpdateUser(ctx, newAccount) if err != nil { return false, err } return true, nil } } return false, bmclibErrs.ErrNoUserSlotsAvailable } // // UserUpdate updates a user password and role func (a *ASRockRack) UserUpdate(ctx context.Context, user, pass, role string) (ok bool, err error) { if !internal.StringInSlice(role, validRoles) { return false, bmclibErrs.ErrInvalidUserRole } if user == "" || pass == "" || role == "" { return false, bmclibErrs.ErrUserParamsRequired } accounts, err := a.listUsers(ctx) if err != nil { return false, errors.Wrap(bmclibErrs.ErrRetrievingUserAccounts, err.Error()) } role = strings.ToLower(role) // identify account slot not in use for _, account := range accounts { account := account if account.Name == user { user := newUserAccount(account.ID, user, pass, role) user.AccessByChannel = account.AccessByChannel user.PrivilegeByChannel = account.PrivilegeByChannel user.Privilege = role if role == "administrator" { user.PrivilegeLimitSerial = "none" user.UserOperation = 1 user.CreationTime = 6000 // doesn't mean anything. } err := a.createUpdateUser(ctx, user) if err != nil { return false, errors.Wrap(bmclibErrs.ErrUserAccountUpdate, err.Error()) } return true, nil } } return ok, errors.Wrap(bmclibErrs.ErrUserAccountNotFound, user) } // newUserAccount returns a user account object populated with the given attributes and certain defaults // // note: the role parameter must be validated before being passed to this constructor func newUserAccount(id int, user, pass, role string) *UserAccount { return &UserAccount{ ID: id, Name: user, Access: 1, // Access enabled Kvm: 1, Vmedia: 1, NetworkPrivilege: role, FixedUserCount: 2, // No idea what this is about OEMProprietaryLevelPrivilege: 1, PrivilegeLimitSerial: role, SSHKey: "Not Available", CreationTime: 0, Changepassword: 1, UserOperation: 0, Password: pass, ConfirmPassword: pass, PasswordSize: "bytes_16", // bytes_20 for larger passwords EmailFormat: "AMI-Format", } } ================================================ FILE: providers/asrockrack/user_test.go ================================================ package asrockrack import ( "context" "errors" "net/http" "os" "testing" "github.com/stretchr/testify/assert" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" ) // NOTE: user accounts are defined in mock_test.go as JSON payload in the userPayload var type testCase struct { user string pass string role string ok bool err error tName string } var ( // common set of test cases testCases = []testCase{ { "foo", "baz", "", false, bmclibErrs.ErrInvalidUserRole, "role not defined", }, { "foo", "", "Administrator", false, bmclibErrs.ErrUserParamsRequired, "param not defined", }, } ) func Test_UserRead(t *testing.T) { expected := []map[string]string{ { "RoleID": "administrator", "ID": "2", "Name": "admin", }, { "ID": "3", "Name": "foo", "RoleID": "administrator", }, } err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } users, err := aClient.UserRead(context.TODO()) if err != nil { t.Error(err) } assert.Equal(t, expected, users) for _, tt := range testCases { ok, err := aClient.UserCreate(context.TODO(), tt.user, tt.pass, tt.role) assert.Equal(t, errors.Is(err, tt.err), true, tt.tName) assert.Equal(t, tt.ok, ok, tt.tName) } // test account retrieval failure error os.Setenv("TEST_FAIL_QUERY", "womp womp") defer os.Unsetenv("TEST_FAIL_QUERY") _, err = aClient.UserRead(context.TODO()) assert.Equal(t, errors.Is(err, bmclibErrs.ErrRetrievingUserAccounts), true) } func Test_UserCreate(t *testing.T) { tests := testCases tests = append(tests, []testCase{{ "root", "calvin", "Administrator", true, nil, "user account is created", }, { "admin", "foo", "Administrator", false, bmclibErrs.ErrUserAccountExists, "account already exists", }, }..., ) err := aClient.httpsLogin(context.TODO()) if err != nil { t.Error(err) } for _, tt := range tests { ok, err := aClient.UserCreate(context.TODO(), tt.user, tt.pass, tt.role) assert.Equal(t, errors.Is(err, tt.err), true, tt.tName) assert.Equal(t, tt.ok, ok, tt.tName) } } func Test_UserUpdate(t *testing.T) { tests := testCases tests = append(tests, []testCase{ { "admin", "calvin", "Administrator", true, nil, "user account is updated", }, { "badmin", "calvin", "Administrator", false, bmclibErrs.ErrUserAccountNotFound, "user account not present", }, }..., ) err := aClient.httpsLogin(context.TODO()) if err != nil { t.Error(err) } for _, tt := range tests { ok, err := aClient.UserUpdate(context.TODO(), tt.user, tt.pass, tt.role) assert.Equal(t, errors.Is(err, tt.err), true, tt.tName) assert.Equal(t, tt.ok, ok, tt.tName) } } func Test_createUser(t *testing.T) { err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } account := &UserAccount{ ID: 3, Name: "foobar", Access: 1, Kvm: 1, Vmedia: 1, NetworkPrivilege: "administrator", FixedUserCount: 2, OEMProprietaryLevelPrivilege: 1, PrivilegeLimitSerial: "none", SSHKey: "Not Available", CreationTime: 4802, Changepassword: 0, UserOperation: 0, Password: "", ConfirmPassword: "", PasswordSize: "", } err = aClient.createUpdateUser(context.TODO(), account) if err != nil { t.Error(err) } assert.Equal(t, "/api/settings/users/3", httpRequestTestVar.URL.String()) assert.Equal(t, http.MethodPut, httpRequestTestVar.Method) var contentType string for k, v := range httpRequestTestVar.Header { if k == "Content-Type" { contentType = v[0] } } assert.Equal(t, "application/json", contentType) } func Test_userAccounts(t *testing.T) { err := aClient.httpsLogin(context.TODO()) if err != nil { t.Errorf("login: %s", err.Error()) } account0 := &UserAccount{ ID: 1, Name: "anonymous", Access: 0, Kvm: 1, Vmedia: 1, NetworkPrivilege: "administrator", FixedUserCount: 2, OEMProprietaryLevelPrivilege: 1, PrivilegeLimitSerial: "none", SSHKey: "Not Available", CreationTime: 4802, Changepassword: 0, UserOperation: 0, Password: "", ConfirmPassword: "", PasswordSize: "", EmailFormat: "ami_format", } accounts, err := aClient.listUsers(context.TODO()) if err != nil { t.Error(err) } assert.Equal(t, 10, len(accounts)) assert.Equal(t, account0, accounts[0]) } ================================================ FILE: providers/dell/firmware.go ================================================ package dell import ( "context" "encoding/json" "fmt" "io" "os" "strings" "time" "github.com/bmc-toolbox/bmclib/v2/constants" bmcliberrs "github.com/bmc-toolbox/bmclib/v2/errors" rfw "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" "github.com/bmc-toolbox/common" "github.com/pkg/errors" "github.com/stmcginnis/gofish/schemas" ) // bmc client interface implementations methods func (c *Conn) FirmwareInstallSteps(ctx context.Context, component string) ([]constants.FirmwareInstallStep, error) { if err := c.deviceSupported(ctx); err != nil { return nil, bmcliberrs.NewErrUnsupportedHardware(err.Error()) } return []constants.FirmwareInstallStep{ constants.FirmwareInstallStepUploadInitiateInstall, constants.FirmwareInstallStepInstallStatus, }, nil } func (c *Conn) FirmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File) (taskID string, err error) { if err := c.deviceSupported(ctx); err != nil { return "", bmcliberrs.NewErrUnsupportedHardware(err.Error()) } // // expect atleast 5 minutes left in the deadline to proceed with the upload d, _ := ctx.Deadline() if time.Until(d) < 10*time.Minute { return "", errors.New("remaining context deadline insufficient to perform update: " + time.Until(d).String()) } // list current tasks on BMC tasks, err := c.redfishwrapper.Tasks(ctx) if err != nil { return "", errors.Wrap(err, "error listing bmc redfish tasks") } // validate a new firmware install task can be queued if err := c.checkQueueability(component, tasks); err != nil { return "", errors.Wrap(bmcliberrs.ErrFirmwareInstall, err.Error()) } params := &rfw.RedfishUpdateServiceParameters{ Targets: []string{}, OperationApplyTime: constants.OnReset, Oem: []byte(`{}`), } return c.redfishwrapper.FirmwareUpload(ctx, file, params) } // checkQueueability returns an error if an existing firmware task is in progress for the given component func (c *Conn) checkQueueability(component string, tasks []*schemas.Task) error { errTaskActive := errors.New("A firmware job was found active for component: " + component) // Redfish on the Idrac names firmware install tasks in this manner. taskNameMap := map[string]string{ common.SlugBIOS: "Firmware Update: BIOS", common.SlugBMC: "Firmware Update: iDRAC with Lifecycle Controller", common.SlugNIC: "Firmware Update: Network", common.SlugDrive: "Firmware Update: Serial ATA", common.SlugStorageController: "Firmware Update: SAS RAID", } for _, t := range tasks { if t.Name == taskNameMap[strings.ToUpper(component)] { // taskInfo returned in error if any. taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", t.ID, t.TaskState, t.TaskStatus) // convert redfish task state to bmclib state convstate := c.redfishwrapper.ConvertTaskState(string(t.TaskState)) // check if task is active based on converted state active, err := c.redfishwrapper.TaskStateActive(convstate) if err != nil { return errors.Wrap(err, taskInfo) } if active { return errors.Wrap(errTaskActive, taskInfo) } } } return nil } // FirmwareTaskStatus returns the status of a firmware related task queued on the BMC. func (c *Conn) FirmwareTaskStatus(ctx context.Context, kind constants.FirmwareInstallStep, component, taskID, installVersion string) (state constants.TaskState, status string, err error) { if err := c.deviceSupported(ctx); err != nil { return "", "", bmcliberrs.NewErrUnsupportedHardware(err.Error()) } // Dell jobs are turned into Redfish tasks on the idrac // once the Redfish task completes successfully, the Redfish task is purged, // and the dell Job stays around. task, err := c.redfishwrapper.Task(ctx, taskID) if err != nil { if errors.Is(err, bmcliberrs.ErrTaskNotFound) { return c.statusFromJob(taskID) } return "", "", err } return c.statusFromTaskOem(taskID, task.OEM) } func (c *Conn) statusFromJob(taskID string) (constants.TaskState, string, error) { job, err := c.job(taskID) if err != nil { return "", "", err } s := strings.ToLower(job.JobState) state := c.redfishwrapper.ConvertTaskState(s) status := fmt.Sprintf( "id: %s, state: %s, status: %s, progress: %d%%", taskID, job.JobState, job.Message, job.PercentComplete, ) return state, status, nil } func (c *Conn) statusFromTaskOem(taskID string, oem json.RawMessage) (constants.TaskState, string, error) { data, err := convFirmwareTaskOem(oem) if err != nil { return "", "", err } s := strings.ToLower(data.Dell.JobState) state := c.redfishwrapper.ConvertTaskState(s) status := fmt.Sprintf( "id: %s, state: %s, status: %s, progress: %d%%", taskID, data.Dell.JobState, data.Dell.Message, data.Dell.PercentComplete, ) return state, status, nil } func (c *Conn) job(jobID string) (*Dell, error) { errLookup := errors.New("error querying dell job: " + jobID) endpoint := "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/" + jobID resp, err := c.redfishwrapper.Get(endpoint) if err != nil { return nil, errors.Wrap(errLookup, err.Error()) } if resp.StatusCode != 200 { return nil, errors.Wrap(errLookup, "unexpected status code: "+resp.Status) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(errLookup, err.Error()) } dell := &Dell{} err = json.Unmarshal(body, &dell) if err != nil { return nil, errors.Wrap(errLookup, err.Error()) } return dell, nil } type oem struct { Dell `json:"Dell"` } type Dell struct { OdataType string `json:"@odata.type"` CompletionTime interface{} `json:"CompletionTime"` Description string `json:"Description"` EndTime string `json:"EndTime"` ID string `json:"Id"` JobState string `json:"JobState"` JobType string `json:"JobType"` Message string `json:"Message"` MessageArgs []interface{} `json:"MessageArgs"` MessageID string `json:"MessageId"` Name string `json:"Name"` PercentComplete int `json:"PercentComplete"` StartTime string `json:"StartTime"` TargetSettingsURI interface{} `json:"TargetSettingsURI"` } func convFirmwareTaskOem(oemdata json.RawMessage) (oem, error) { oem := oem{} errTaskOem := errors.New("error in Task Oem data: " + string(oemdata)) if len(oemdata) == 0 || string(oemdata) == `{}` { return oem, errors.Wrap(errTaskOem, "empty oem data") } if err := json.Unmarshal(oemdata, &oem); err != nil { return oem, errors.Wrap(errTaskOem, "failed to unmarshal: "+err.Error()) } if oem.Dell.Description == "" || oem.Dell.JobState == "" { return oem, errors.Wrap(errTaskOem, "invalid oem data") } if oem.Dell.JobType != "FirmwareUpdate" { return oem, errors.Wrap(errTaskOem, "unexpected job type: "+oem.Dell.JobType) } return oem, nil } ================================================ FILE: providers/dell/firmware_test.go ================================================ package dell import ( "testing" "github.com/stretchr/testify/assert" ) func TestConvFirmwareTaskOem(t *testing.T) { testCases := []struct { name string oemdata []byte expectedJob oem expectedErr string }{ { name: "Valid OEM data", oemdata: []byte(`{ "Dell": { "@odata.type": "#DellJob.v1_4_0.DellJob", "CompletionTime": null, "Description": "Job Instance", "EndTime": "TIME_NA", "Id": "JID_005950769310", "JobState": "Scheduled", "JobType": "FirmwareUpdate", "Message": "Task successfully scheduled.", "MessageArgs": [], "MessageId": "IDRAC.2.8.JCP001", "Name": "Firmware Update: BIOS", "PercentComplete": 0, "StartTime": "TIME_NOW", "TargetSettingsURI": null } }`), expectedJob: oem{ Dell{ OdataType: "#DellJob.v1_4_0.DellJob", CompletionTime: nil, Description: "Job Instance", EndTime: "TIME_NA", ID: "JID_005950769310", JobState: "Scheduled", JobType: "FirmwareUpdate", Message: "Task successfully scheduled.", MessageArgs: []interface{}{}, MessageID: "IDRAC.2.8.JCP001", Name: "Firmware Update: BIOS", PercentComplete: 0, StartTime: "TIME_NOW", TargetSettingsURI: nil, }, }, expectedErr: "", }, { name: "Empty OEM data", oemdata: []byte(`{}`), expectedJob: oem{}, expectedErr: "empty oem data", }, { name: "Invalid OEM data", oemdata: []byte(`{"InvalidKey": "InvalidValue"}`), expectedJob: oem{}, expectedErr: "invalid oem data", }, { name: "Unexpected job type", oemdata: []byte(`{ "Dell": { "JobType": "InvalidJobType", "Description": "Job Instance", "JobState": "Scheduled" } }`), expectedJob: oem{}, expectedErr: "unexpected job type", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { job, err := convFirmwareTaskOem(tc.oemdata) if tc.expectedErr == "" { assert.NoError(t, err) assert.Equal(t, tc.expectedJob, job) } else { assert.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErr) } }) } } ================================================ FILE: providers/dell/fixtures/serviceroot.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ServiceRoot.ServiceRoot", "@odata.id": "/redfish/v1", "@odata.type": "#ServiceRoot.v1_6_0.ServiceRoot", "AccountService": { "@odata.id": "/redfish/v1/AccountService" }, "CertificateService": { "@odata.id": "/redfish/v1/CertificateService" }, "Chassis": { "@odata.id": "/redfish/v1/Chassis" }, "Description": "Root Service", "EventService": { "@odata.id": "/redfish/v1/EventService" }, "Fabrics": { "@odata.id": "/redfish/v1/Fabrics" }, "Id": "RootService", "JobService": { "@odata.id": "/redfish/v1/JobService" }, "JsonSchemas": { "@odata.id": "/redfish/v1/JsonSchemas" }, "Links": { "Sessions": { "@odata.id": "/redfish/v1/SessionService/Sessions" } }, "Managers": { "@odata.id": "/redfish/v1/Managers" }, "Name": "Root Service", "Oem": { "Dell": { "@odata.context": "/redfish/v1/$metadata#DellServiceRoot.DellServiceRoot", "@odata.type": "#DellServiceRoot.v1_0_0.DellServiceRoot", "IsBranded": 0, "ManagerMACAddress": "d0:8e:79:bb:3e:ea", "ServiceTag": "FOOBAR" } }, "Product": "Integrated Dell Remote Access Controller", "ProtocolFeaturesSupported": { "ExcerptQuery": false, "ExpandQuery": { "ExpandAll": true, "Levels": true, "Links": true, "MaxLevels": 1, "NoLinks": true }, "FilterQuery": true, "OnlyMemberQuery": true, "SelectQuery": true }, "RedfishVersion": "1.9.0", "Registries": { "@odata.id": "/redfish/v1/Registries" }, "SessionService": { "@odata.id": "/redfish/v1/SessionService" }, "Systems": { "@odata.id": "/redfish/v1/Systems" }, "Tasks": { "@odata.id": "/redfish/v1/TaskService" }, "TelemetryService": { "@odata.id": "/redfish/v1/TelemetryService" }, "UpdateService": { "@odata.id": "/redfish/v1/UpdateService" }, "Vendor": "Dell" } ================================================ FILE: providers/dell/fixtures/systems.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ComputerSystemCollection.ComputerSystemCollection", "@odata.id": "/redfish/v1/Systems", "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", "Description": "Collection of Computer Systems", "Members": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1" } ], "Members@odata.count": 1, "Name": "Computer System Collection" } ================================================ FILE: providers/dell/fixtures/systems_embedded.1.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ComputerSystem.ComputerSystem", "@odata.id": "/redfish/v1/Systems/System.Embedded.1", "@odata.type": "#ComputerSystem.v1_12_0.ComputerSystem", "Actions": { "#ComputerSystem.Reset": { "target": "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset", "ResetType@Redfish.AllowableValues": [ "On", "ForceOff", "ForceRestart", "GracefulRestart", "GracefulShutdown", "PushPowerButton", "Nmi", "PowerCycle" ] } }, "AssetTag": "", "Bios": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Bios" }, "BiosVersion": "2.6.6", "Boot": { "BootOptions": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/BootOptions" }, "Certificates": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Boot/Certificates" }, "BootOrder": [ "HardDisk.List.1-1", "NIC.Slot.3-1-1" ], "BootOrder@odata.count": 2, "BootSourceOverrideEnabled": "Disabled", "BootSourceOverrideMode": "Legacy", "BootSourceOverrideTarget": "None", "UefiTargetBootSourceOverride": null, "BootSourceOverrideTarget@Redfish.AllowableValues": [ "None", "Pxe", "Floppy", "Cd", "Hdd", "BiosSetup", "Utilities", "UefiTarget", "SDCard", "UefiHttp" ] }, "Description": "Computer System which represents a machine (physical or virtual) and the local resources such as memory, cpu and other devices that can be accessed from that machine.", "EthernetInterfaces": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/EthernetInterfaces" }, "HostName": "foobaz", "HostWatchdogTimer": { "FunctionEnabled": false, "Status": { "State": "Disabled" }, "TimeoutAction": "None" }, "HostingRoles": [], "HostingRoles@odata.count": 0, "Id": "System.Embedded.1", "IndicatorLED": "Lit", "Links": { "Chassis": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1" } ], "Chassis@odata.count": 1, "CooledBy": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/0" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/1" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/2" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/3" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/4" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/5" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/6" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/7" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/8" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/9" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/10" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/11" } ], "CooledBy@odata.count": 12, "ManagedBy": [ { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1" } ], "ManagedBy@odata.count": 1, "Oem": { "Dell": { "@odata.type": "#DellOem.v1_2_0.DellOemLinks", "BootOrder": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBootSources" }, "DellBootSources": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBootSources" }, "DellSoftwareInstallationService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSoftwareInstallationService" }, "DellVideoCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellVideo" }, "DellChassisCollection": { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Oem/Dell/DellChassis" }, "DellPresenceAndStatusSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellPresenceAndStatusSensors" }, "DellSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSensors" }, "DellRollupStatusCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellRollupStatus" }, "DellPSNumericSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellPSNumericSensors" }, "DellVideoNetworkCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellVideoNetwork" }, "DellOSDeploymentService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellOSDeploymentService" }, "DellMetricService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellMetricService" }, "DellGPUSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellGPUSensors" }, "DellRaidService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellRaidService" }, "DellNumericSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellNumericSensors" }, "DellBIOSService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBIOSService" }, "DellSlotCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSlots" } } }, "PoweredBy": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power#/PowerSupplies/0" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power#/PowerSupplies/1" } ], "PoweredBy@odata.count": 2 }, "Manufacturer": "Dell Inc.", "Memory": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Memory" }, "MemorySummary": { "MemoryMirroring": "System", "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" }, "TotalSystemMemoryGiB": 256 }, "Model": "PowerEdge R6515", "Name": "System", "NetworkInterfaces": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/NetworkInterfaces" }, "Oem": { "Dell": { "@odata.type": "#DellOem.v1_2_0.DellOemResources", "DellSystem": { "BIOSReleaseDate": "01/13/2022", "BaseBoardChassisSlot": "NA", "BatteryRollupStatus": "OK", "BladeGeometry": "NotApplicable", "CMCIP": null, "CPURollupStatus": "OK", "ChassisModel": "", "ChassisName": "Main System Chassis", "ChassisServiceTag": "FOOBAR", "ChassisSystemHeightUnit": 1, "CurrentRollupStatus": "OK", "EstimatedExhaustTemperatureCelsius": 255, "EstimatedSystemAirflowCFM": 255, "ExpressServiceCode": "1234567819", "FanRollupStatus": "OK", "Id": "System.Embedded.1", "IDSDMRollupStatus": null, "IntrusionRollupStatus": "OK", "IsOEMBranded": "False", "LastSystemInventoryTime": "2023-04-28T04:00:49+00:00", "LastUpdateTime": "2022-10-11T21:35:12+00:00", "LicensingRollupStatus": "OK", "ManagedSystemSize": "1 U", "MaxCPUSockets": 1, "MaxDIMMSlots": 16, "MaxPCIeSlots": 5, "MemoryOperationMode": "OptimizerMode", "Name": "DellSystem", "NodeID": "FOOBAR", "PSRollupStatus": "OK", "PlatformGUID": "33435a4f-c0c6-4780-5210-00304c4c4544", "PopulatedDIMMSlots": 8, "PopulatedPCIeSlots": 2, "PowerCapEnabledState": "Disabled", "SDCardRollupStatus": null, "SELRollupStatus": "OK", "ServerAllocationWatts": null, "StorageRollupStatus": "OK", "SysMemErrorMethodology": "Multi-bitECC", "SysMemFailOverState": "NotInUse", "SysMemLocation": "SystemBoardOrMotherboard", "SysMemPrimaryStatus": "OK", "SystemGeneration": "15G Monolithic", "SystemID": 2300, "SystemRevision": "I", "TempRollupStatus": "OK", "TempStatisticsRollupStatus": "OK", "UUID": "4c4c4544-0030-5210-8047-c6c04f5a4333", "VoltRollupStatus": "OK", "smbiosGUID": "44454c4c-3000-1052-8047-c6c04f5a4333", "@odata.context": "/redfish/v1/$metadata#DellSystem.DellSystem", "@odata.type": "#DellSystem.v1_3_0.DellSystem", "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSystem/System.Embedded.1" } } }, "PCIeDevices": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/70-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/69-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/194-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/72-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/195-0" } ], "PCIeDevices@odata.count": 34, "PCIeFunctions": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-3/PCIeFunctions/0-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-2/PCIeFunctions/0-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-4/PCIeFunctions/0-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-7/PCIeFunctions/0-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-1/PCIeFunctions/0-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20/PCIeFunctions/0-20-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-8/PCIeFunctions/0-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20/PCIeFunctions/0-20-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/70-0/PCIeFunctions/70-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0/PCIeFunctions/65-0-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0/PCIeFunctions/65-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/69-0/PCIeFunctions/69-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-2/PCIeFunctions/192-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-1/PCIeFunctions/192-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/194-0/PCIeFunctions/194-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/4-0/PCIeFunctions/4-0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-3/PCIeFunctions/192-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-4/PCIeFunctions/192-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-8/PCIeFunctions/192-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-0/PCIeFunctions/192-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-7/PCIeFunctions/192-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/1-0/PCIeFunctions/1-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-3/PCIeFunctions/128-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-2/PCIeFunctions/128-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/72-0/PCIeFunctions/72-0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-4/PCIeFunctions/128-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-7/PCIeFunctions/128-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-1/PCIeFunctions/128-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-8/PCIeFunctions/128-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-3/PCIeFunctions/64-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-2/PCIeFunctions/64-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-4/PCIeFunctions/64-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-7/PCIeFunctions/64-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-1/PCIeFunctions/64-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-8/PCIeFunctions/64-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/195-0/PCIeFunctions/195-0-0" } ], "PCIeFunctions@odata.count": 36, "PartNumber": "07PXPYA01", "PowerState": "On", "ProcessorSummary": { "Count": 1, "LogicalProcessorCount": 32, "Model": "AMD EPYC 7502P 32-Core Processor", "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" } }, "Processors": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Processors" }, "SKU": "FOOBAR", "SecureBoot": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/SecureBoot" }, "SerialNumber": "FOOBAR123", "SimpleStorage": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/SimpleStorage" }, "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" }, "Storage": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage" }, "SystemType": "Physical", "TrustedModules": [ { "FirmwareVersion": "1.3.2.8", "InterfaceType": "TPM2_0", "Status": { "State": "Enabled" } } ], "TrustedModules@odata.count": 1, "UUID": "4c4c4544-0030-5210-8047-c6c04f5a4333" } ================================================ FILE: providers/dell/fixtures/systems_embedded_no_manufacturer.1.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ComputerSystem.ComputerSystem", "@odata.id": "/redfish/v1/Systems/System.Embedded.1", "@odata.type": "#ComputerSystem.v1_12_0.ComputerSystem", "Actions": { "#ComputerSystem.Reset": { "target": "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset", "ResetType@Redfish.AllowableValues": [ "On", "ForceOff", "ForceRestart", "GracefulRestart", "GracefulShutdown", "PushPowerButton", "Nmi", "PowerCycle" ] } }, "AssetTag": "", "Bios": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Bios" }, "BiosVersion": "2.6.6", "Boot": { "BootOptions": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/BootOptions" }, "Certificates": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Boot/Certificates" }, "BootOrder": [ "HardDisk.List.1-1", "NIC.Slot.3-1-1" ], "BootOrder@odata.count": 2, "BootSourceOverrideEnabled": "Disabled", "BootSourceOverrideMode": "Legacy", "BootSourceOverrideTarget": "None", "UefiTargetBootSourceOverride": null, "BootSourceOverrideTarget@Redfish.AllowableValues": [ "None", "Pxe", "Floppy", "Cd", "Hdd", "BiosSetup", "Utilities", "UefiTarget", "SDCard", "UefiHttp" ] }, "Description": "Computer System which represents a machine (physical or virtual) and the local resources such as memory, cpu and other devices that can be accessed from that machine.", "EthernetInterfaces": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/EthernetInterfaces" }, "HostName": "foobaz", "HostWatchdogTimer": { "FunctionEnabled": false, "Status": { "State": "Disabled" }, "TimeoutAction": "None" }, "HostingRoles": [], "HostingRoles@odata.count": 0, "Id": "System.Embedded.1", "IndicatorLED": "Lit", "Links": { "Chassis": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1" } ], "Chassis@odata.count": 1, "CooledBy": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/0" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/1" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/2" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/3" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/4" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/5" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/6" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/7" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/8" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/9" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/10" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/11" } ], "CooledBy@odata.count": 12, "ManagedBy": [ { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1" } ], "ManagedBy@odata.count": 1, "Oem": { "Dell": { "@odata.type": "#DellOem.v1_2_0.DellOemLinks", "BootOrder": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBootSources" }, "DellBootSources": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBootSources" }, "DellSoftwareInstallationService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSoftwareInstallationService" }, "DellVideoCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellVideo" }, "DellChassisCollection": { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Oem/Dell/DellChassis" }, "DellPresenceAndStatusSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellPresenceAndStatusSensors" }, "DellSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSensors" }, "DellRollupStatusCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellRollupStatus" }, "DellPSNumericSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellPSNumericSensors" }, "DellVideoNetworkCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellVideoNetwork" }, "DellOSDeploymentService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellOSDeploymentService" }, "DellMetricService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellMetricService" }, "DellGPUSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellGPUSensors" }, "DellRaidService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellRaidService" }, "DellNumericSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellNumericSensors" }, "DellBIOSService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBIOSService" }, "DellSlotCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSlots" } } }, "PoweredBy": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power#/PowerSupplies/0" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power#/PowerSupplies/1" } ], "PoweredBy@odata.count": 2 }, "Manufacturer": "", "Memory": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Memory" }, "MemorySummary": { "MemoryMirroring": "System", "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" }, "TotalSystemMemoryGiB": 256 }, "Model": "PowerEdge R6515", "Name": "System", "NetworkInterfaces": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/NetworkInterfaces" }, "Oem": { "Dell": { "@odata.type": "#DellOem.v1_2_0.DellOemResources", "DellSystem": { "BIOSReleaseDate": "01/13/2022", "BaseBoardChassisSlot": "NA", "BatteryRollupStatus": "OK", "BladeGeometry": "NotApplicable", "CMCIP": null, "CPURollupStatus": "OK", "ChassisModel": "", "ChassisName": "Main System Chassis", "ChassisServiceTag": "FOOBAR", "ChassisSystemHeightUnit": 1, "CurrentRollupStatus": "OK", "EstimatedExhaustTemperatureCelsius": 255, "EstimatedSystemAirflowCFM": 255, "ExpressServiceCode": "1234567819", "FanRollupStatus": "OK", "Id": "System.Embedded.1", "IDSDMRollupStatus": null, "IntrusionRollupStatus": "OK", "IsOEMBranded": "False", "LastSystemInventoryTime": "2023-04-28T04:00:49+00:00", "LastUpdateTime": "2022-10-11T21:35:12+00:00", "LicensingRollupStatus": "OK", "ManagedSystemSize": "1 U", "MaxCPUSockets": 1, "MaxDIMMSlots": 16, "MaxPCIeSlots": 5, "MemoryOperationMode": "OptimizerMode", "Name": "DellSystem", "NodeID": "FOOBAR", "PSRollupStatus": "OK", "PlatformGUID": "33435a4f-c0c6-4780-5210-00304c4c4544", "PopulatedDIMMSlots": 8, "PopulatedPCIeSlots": 2, "PowerCapEnabledState": "Disabled", "SDCardRollupStatus": null, "SELRollupStatus": "OK", "ServerAllocationWatts": null, "StorageRollupStatus": "OK", "SysMemErrorMethodology": "Multi-bitECC", "SysMemFailOverState": "NotInUse", "SysMemLocation": "SystemBoardOrMotherboard", "SysMemPrimaryStatus": "OK", "SystemGeneration": "15G Monolithic", "SystemID": 2300, "SystemRevision": "I", "TempRollupStatus": "OK", "TempStatisticsRollupStatus": "OK", "UUID": "4c4c4544-0030-5210-8047-c6c04f5a4333", "VoltRollupStatus": "OK", "smbiosGUID": "44454c4c-3000-1052-8047-c6c04f5a4333", "@odata.context": "/redfish/v1/$metadata#DellSystem.DellSystem", "@odata.type": "#DellSystem.v1_3_0.DellSystem", "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSystem/System.Embedded.1" } } }, "PCIeDevices": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/70-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/69-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/194-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/72-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/195-0" } ], "PCIeDevices@odata.count": 34, "PCIeFunctions": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-3/PCIeFunctions/0-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-2/PCIeFunctions/0-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-4/PCIeFunctions/0-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-7/PCIeFunctions/0-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-1/PCIeFunctions/0-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20/PCIeFunctions/0-20-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-8/PCIeFunctions/0-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20/PCIeFunctions/0-20-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/70-0/PCIeFunctions/70-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0/PCIeFunctions/65-0-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0/PCIeFunctions/65-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/69-0/PCIeFunctions/69-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-2/PCIeFunctions/192-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-1/PCIeFunctions/192-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/194-0/PCIeFunctions/194-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/4-0/PCIeFunctions/4-0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-3/PCIeFunctions/192-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-4/PCIeFunctions/192-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-8/PCIeFunctions/192-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-0/PCIeFunctions/192-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-7/PCIeFunctions/192-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/1-0/PCIeFunctions/1-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-3/PCIeFunctions/128-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-2/PCIeFunctions/128-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/72-0/PCIeFunctions/72-0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-4/PCIeFunctions/128-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-7/PCIeFunctions/128-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-1/PCIeFunctions/128-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-8/PCIeFunctions/128-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-3/PCIeFunctions/64-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-2/PCIeFunctions/64-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-4/PCIeFunctions/64-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-7/PCIeFunctions/64-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-1/PCIeFunctions/64-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-8/PCIeFunctions/64-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/195-0/PCIeFunctions/195-0-0" } ], "PCIeFunctions@odata.count": 36, "PartNumber": "07PXPYA01", "PowerState": "On", "ProcessorSummary": { "Count": 1, "LogicalProcessorCount": 32, "Model": "AMD EPYC 7502P 32-Core Processor", "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" } }, "Processors": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Processors" }, "SKU": "FOOBAR", "SecureBoot": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/SecureBoot" }, "SerialNumber": "FOOBAR123", "SimpleStorage": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/SimpleStorage" }, "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" }, "Storage": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage" }, "SystemType": "Physical", "TrustedModules": [ { "FirmwareVersion": "1.3.2.8", "InterfaceType": "TPM2_0", "Status": { "State": "Enabled" } } ], "TrustedModules@odata.count": 1, "UUID": "4c4c4544-0030-5210-8047-c6c04f5a4333" } ================================================ FILE: providers/dell/fixtures/systems_embedded_not_dell.1.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ComputerSystem.ComputerSystem", "@odata.id": "/redfish/v1/Systems/System.Embedded.1", "@odata.type": "#ComputerSystem.v1_12_0.ComputerSystem", "Actions": { "#ComputerSystem.Reset": { "target": "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset", "ResetType@Redfish.AllowableValues": [ "On", "ForceOff", "ForceRestart", "GracefulRestart", "GracefulShutdown", "PushPowerButton", "Nmi", "PowerCycle" ] } }, "AssetTag": "", "Bios": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Bios" }, "BiosVersion": "2.6.6", "Boot": { "BootOptions": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/BootOptions" }, "Certificates": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Boot/Certificates" }, "BootOrder": [ "HardDisk.List.1-1", "NIC.Slot.3-1-1" ], "BootOrder@odata.count": 2, "BootSourceOverrideEnabled": "Disabled", "BootSourceOverrideMode": "Legacy", "BootSourceOverrideTarget": "None", "UefiTargetBootSourceOverride": null, "BootSourceOverrideTarget@Redfish.AllowableValues": [ "None", "Pxe", "Floppy", "Cd", "Hdd", "BiosSetup", "Utilities", "UefiTarget", "SDCard", "UefiHttp" ] }, "Description": "Computer System which represents a machine (physical or virtual) and the local resources such as memory, cpu and other devices that can be accessed from that machine.", "EthernetInterfaces": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/EthernetInterfaces" }, "HostName": "foobaz", "HostWatchdogTimer": { "FunctionEnabled": false, "Status": { "State": "Disabled" }, "TimeoutAction": "None" }, "HostingRoles": [], "HostingRoles@odata.count": 0, "Id": "System.Embedded.1", "IndicatorLED": "Lit", "Links": { "Chassis": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1" } ], "Chassis@odata.count": 1, "CooledBy": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/0" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/1" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/2" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/3" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/4" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/5" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/6" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/7" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/8" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/9" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/10" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Thermal#/Fans/11" } ], "CooledBy@odata.count": 12, "ManagedBy": [ { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1" } ], "ManagedBy@odata.count": 1, "Oem": { "Dell": { "@odata.type": "#DellOem.v1_2_0.DellOemLinks", "BootOrder": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBootSources" }, "DellBootSources": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBootSources" }, "DellSoftwareInstallationService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSoftwareInstallationService" }, "DellVideoCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellVideo" }, "DellChassisCollection": { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Oem/Dell/DellChassis" }, "DellPresenceAndStatusSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellPresenceAndStatusSensors" }, "DellSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSensors" }, "DellRollupStatusCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellRollupStatus" }, "DellPSNumericSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellPSNumericSensors" }, "DellVideoNetworkCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellVideoNetwork" }, "DellOSDeploymentService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellOSDeploymentService" }, "DellMetricService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellMetricService" }, "DellGPUSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellGPUSensors" }, "DellRaidService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellRaidService" }, "DellNumericSensorCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellNumericSensors" }, "DellBIOSService": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellBIOSService" }, "DellSlotCollection": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSlots" } } }, "PoweredBy": [ { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power#/PowerSupplies/0" }, { "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power#/PowerSupplies/1" } ], "PoweredBy@odata.count": 2 }, "Manufacturer": "bmclib", "Memory": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Memory" }, "MemorySummary": { "MemoryMirroring": "System", "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" }, "TotalSystemMemoryGiB": 256 }, "Model": "PowerEdge R6515", "Name": "System", "NetworkInterfaces": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/NetworkInterfaces" }, "Oem": { "Dell": { "@odata.type": "#DellOem.v1_2_0.DellOemResources", "DellSystem": { "BIOSReleaseDate": "01/13/2022", "BaseBoardChassisSlot": "NA", "BatteryRollupStatus": "OK", "BladeGeometry": "NotApplicable", "CMCIP": null, "CPURollupStatus": "OK", "ChassisModel": "", "ChassisName": "Main System Chassis", "ChassisServiceTag": "FOOBAR", "ChassisSystemHeightUnit": 1, "CurrentRollupStatus": "OK", "EstimatedExhaustTemperatureCelsius": 255, "EstimatedSystemAirflowCFM": 255, "ExpressServiceCode": "1234567819", "FanRollupStatus": "OK", "Id": "System.Embedded.1", "IDSDMRollupStatus": null, "IntrusionRollupStatus": "OK", "IsOEMBranded": "False", "LastSystemInventoryTime": "2023-04-28T04:00:49+00:00", "LastUpdateTime": "2022-10-11T21:35:12+00:00", "LicensingRollupStatus": "OK", "ManagedSystemSize": "1 U", "MaxCPUSockets": 1, "MaxDIMMSlots": 16, "MaxPCIeSlots": 5, "MemoryOperationMode": "OptimizerMode", "Name": "DellSystem", "NodeID": "FOOBAR", "PSRollupStatus": "OK", "PlatformGUID": "33435a4f-c0c6-4780-5210-00304c4c4544", "PopulatedDIMMSlots": 8, "PopulatedPCIeSlots": 2, "PowerCapEnabledState": "Disabled", "SDCardRollupStatus": null, "SELRollupStatus": "OK", "ServerAllocationWatts": null, "StorageRollupStatus": "OK", "SysMemErrorMethodology": "Multi-bitECC", "SysMemFailOverState": "NotInUse", "SysMemLocation": "SystemBoardOrMotherboard", "SysMemPrimaryStatus": "OK", "SystemGeneration": "15G Monolithic", "SystemID": 2300, "SystemRevision": "I", "TempRollupStatus": "OK", "TempStatisticsRollupStatus": "OK", "UUID": "4c4c4544-0030-5210-8047-c6c04f5a4333", "VoltRollupStatus": "OK", "smbiosGUID": "44454c4c-3000-1052-8047-c6c04f5a4333", "@odata.context": "/redfish/v1/$metadata#DellSystem.DellSystem", "@odata.type": "#DellSystem.v1_3_0.DellSystem", "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Oem/Dell/DellSystem/System.Embedded.1" } } }, "PCIeDevices": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/70-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/69-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/194-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/72-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-2" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-4" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-7" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-8" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/195-0" } ], "PCIeDevices@odata.count": 34, "PCIeFunctions": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-3/PCIeFunctions/0-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-2/PCIeFunctions/0-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-4/PCIeFunctions/0-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-7/PCIeFunctions/0-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-1/PCIeFunctions/0-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20/PCIeFunctions/0-20-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-8/PCIeFunctions/0-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/0-20/PCIeFunctions/0-20-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/70-0/PCIeFunctions/70-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0/PCIeFunctions/65-0-1" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/65-0/PCIeFunctions/65-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/69-0/PCIeFunctions/69-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-2/PCIeFunctions/192-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-1/PCIeFunctions/192-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/194-0/PCIeFunctions/194-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/4-0/PCIeFunctions/4-0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-3/PCIeFunctions/192-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-4/PCIeFunctions/192-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-8/PCIeFunctions/192-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-0/PCIeFunctions/192-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/192-7/PCIeFunctions/192-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/1-0/PCIeFunctions/1-0-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-3/PCIeFunctions/128-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-2/PCIeFunctions/128-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/72-0/PCIeFunctions/72-0-3" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-4/PCIeFunctions/128-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-7/PCIeFunctions/128-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-1/PCIeFunctions/128-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/128-8/PCIeFunctions/128-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-3/PCIeFunctions/64-3-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-2/PCIeFunctions/64-2-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-4/PCIeFunctions/64-4-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-7/PCIeFunctions/64-7-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-1/PCIeFunctions/64-1-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/64-8/PCIeFunctions/64-8-0" }, { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/PCIeDevices/195-0/PCIeFunctions/195-0-0" } ], "PCIeFunctions@odata.count": 36, "PartNumber": "07PXPYA01", "PowerState": "On", "ProcessorSummary": { "Count": 1, "LogicalProcessorCount": 32, "Model": "AMD EPYC 7502P 32-Core Processor", "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" } }, "Processors": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Processors" }, "SKU": "FOOBAR", "SecureBoot": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/SecureBoot" }, "SerialNumber": "FOOBAR123", "SimpleStorage": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/SimpleStorage" }, "Status": { "Health": "OK", "HealthRollup": "OK", "State": "Enabled" }, "Storage": { "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage" }, "SystemType": "Physical", "TrustedModules": [ { "FirmwareVersion": "1.3.2.8", "InterfaceType": "TPM2_0", "Status": { "State": "Enabled" } } ], "TrustedModules@odata.count": 1, "UUID": "4c4c4544-0030-5210-8047-c6c04f5a4333" } ================================================ FILE: providers/dell/idrac.go ================================================ package dell import ( "context" "crypto/x509" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strings" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/bmc-toolbox/common" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" "github.com/pkg/errors" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" ) const ( // ProviderName for the provider Dell implementation ProviderName = "dell" // ProviderProtocol for the provider Dell implementation ProviderProtocol = "redfish" redfishV1Prefix = "/redfish/v1" screenshotEndpoint = "/Dell/Managers/iDRAC.Embedded.1/DellLCService/Actions/DellLCService.ExportServerScreenShot" managerAttributesEndpoint = "/Managers/iDRAC.Embedded.1/Attributes" ) var ( // Features implemented by dell redfish Features = registrar.Features{ providers.FeatureScreenshot, providers.FeaturePowerState, providers.FeaturePowerSet, providers.FeatureFirmwareInstallSteps, providers.FeatureFirmwareUploadInitiateInstall, providers.FeatureFirmwareTaskStatus, providers.FeatureInventoryRead, providers.FeatureBmcReset, providers.FeatureGetBiosConfiguration, providers.FeatureSetBiosConfiguration, providers.FeatureResetBiosConfiguration, } errManufacturerUnknown = errors.New("error identifying device manufacturer") ) type Config struct { HttpClient *http.Client Port string VersionsNotCompatible []string RootCAs *x509.CertPool UseBasicAuth bool } // Option for setting optional Client values type Option func(*Config) func WithHttpClient(httpClient *http.Client) Option { return func(c *Config) { c.HttpClient = httpClient } } func WithPort(port string) Option { return func(c *Config) { c.Port = port } } func WithVersionsNotCompatible(versionsNotCompatible []string) Option { return func(c *Config) { c.VersionsNotCompatible = versionsNotCompatible } } func WithRootCAs(rootCAs *x509.CertPool) Option { return func(c *Config) { c.RootCAs = rootCAs } } func WithUseBasicAuth(useBasicAuth bool) Option { return func(c *Config) { c.UseBasicAuth = useBasicAuth } } // Conn details for redfish client type Conn struct { redfishwrapper *redfishwrapper.Client Log logr.Logger } // New returns connection with a redfish client initialized func New(host, user, pass string, log logr.Logger, opts ...Option) *Conn { defaultConfig := &Config{ HttpClient: httpclient.Build(), Port: "443", VersionsNotCompatible: []string{}, } for _, opt := range opts { opt(defaultConfig) } rfOpts := []redfishwrapper.Option{ redfishwrapper.WithHTTPClient(defaultConfig.HttpClient), redfishwrapper.WithVersionsNotCompatible(defaultConfig.VersionsNotCompatible), redfishwrapper.WithBasicAuthEnabled(defaultConfig.UseBasicAuth), } if defaultConfig.RootCAs != nil { rfOpts = append(rfOpts, redfishwrapper.WithSecureTLS(defaultConfig.RootCAs)) } return &Conn{ Log: log, redfishwrapper: redfishwrapper.NewClient(host, defaultConfig.Port, user, pass, rfOpts...), } } // Open a connection to a BMC via redfish func (c *Conn) Open(ctx context.Context) (err error) { if err := c.redfishwrapper.Open(ctx); err != nil { return err } // because this uses the redfish interface and the redfish interface // is available across various BMC vendors, we verify the device we're connected to is dell. if err := c.deviceSupported(ctx); err != nil { if er := c.redfishwrapper.Close(ctx); er != nil { return fmt.Errorf("%v: %w", err, er) } return err } return nil } func (c *Conn) deviceSupported(ctx context.Context) error { manufacturer, err := c.deviceManufacturer(ctx) if err != nil { return err } m := strings.ToLower(manufacturer) if !strings.Contains(m, common.VendorDell) { return errors.Wrap(bmclibErrs.ErrIncompatibleProvider, m) } return nil } // Close a connection to a BMC via redfish func (c *Conn) Close(ctx context.Context) error { return c.redfishwrapper.Close(ctx) } // Name returns the client provider name. func (c *Conn) Name() string { return ProviderName } // Compatible tests whether a BMC is compatible with the gofish provider func (c *Conn) Compatible(ctx context.Context) bool { err := c.Open(ctx) if err != nil { c.Log.V(2).WithValues( "provider", c.Name(), ).Info("warn", bmclibErrs.ErrCompatibilityCheck.Error(), err.Error()) return false } defer c.Close(ctx) if !c.redfishwrapper.VersionCompatible() { c.Log.V(2).WithValues( "provider", c.Name(), ).Info("info", bmclibErrs.ErrCompatibilityCheck.Error(), "incompatible redfish version") return false } _, err = c.PowerStateGet(ctx) if err != nil { c.Log.V(2).WithValues( "provider", c.Name(), ).Info("warn", bmclibErrs.ErrCompatibilityCheck.Error(), err.Error()) } return err == nil } // PowerStateGet gets the power state of a BMC machine func (c *Conn) PowerStateGet(ctx context.Context) (state string, err error) { return c.redfishwrapper.SystemPowerStatus(ctx) } // PowerSet sets the power state of a server func (c *Conn) PowerSet(ctx context.Context, state string) (ok bool, err error) { return c.redfishwrapper.PowerSet(ctx, state) } // Inventory collects hardware inventory and install firmware information func (c *Conn) Inventory(ctx context.Context) (device *common.Device, err error) { return c.redfishwrapper.Inventory(ctx, false) } // BmcReset power cycles the BMC func (c *Conn) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { return c.redfishwrapper.BMCReset(ctx, resetType) } // GetBiosConfiguration returns the BIOS configuration settings via the BMC func (c *Conn) GetBiosConfiguration(ctx context.Context) (biosConfig map[string]string, err error) { return c.redfishwrapper.GetBiosConfiguration(ctx) } // SetBiosConfiguration sets the BIOS configuration settings via the BMC func (c *Conn) SetBiosConfiguration(ctx context.Context, biosConfig map[string]string) (err error) { return c.redfishwrapper.SetBiosConfiguration(ctx, biosConfig) } // ResetBiosConfiguration resets the BIOS configuration settings back to 'factory defaults' via the BMC func (c *Conn) ResetBiosConfiguration(ctx context.Context) (err error) { return c.redfishwrapper.ResetBiosConfiguration(ctx) } // SendNMI tells the BMC to issue an NMI to the device func (c *Conn) SendNMI(ctx context.Context) error { return c.redfishwrapper.SendNMI(ctx) } // deviceManufacturer returns the device manufacturer and model attributes func (c *Conn) deviceManufacturer(ctx context.Context) (vendor string, err error) { sys, err := c.redfishwrapper.System() if err != nil { return "", errors.Wrap(errManufacturerUnknown, err.Error()) } if sys.Manufacturer != "" { return sys.Manufacturer, nil } return "", errManufacturerUnknown } func (c *Conn) Screenshot(ctx context.Context) (image []byte, fileType string, err error) { fileType = "png" resp, err := c.redfishwrapper.PostWithHeaders( ctx, redfishV1Prefix+screenshotEndpoint, // other FileType parameters are LastCrashScreenshot, Preview json.RawMessage(`{"FileType":"ServerScreenShot"}`), map[string]string{"Content-Type": "application/json"}, ) if err != nil { return nil, "", errors.Wrap(bmclibErrs.ErrScreenshot, err.Error()) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, "", errors.Wrap(bmclibErrs.ErrScreenshot, err.Error()) } if resp.StatusCode != 200 { return nil, "", errors.Wrap(bmclibErrs.ErrScreenshot, resp.Status) } data := &struct { B64encoded string `json:"ServerScreenshotFile"` }{} if err := json.Unmarshal(body, &data); err != nil { return nil, "", errors.Wrap(bmclibErrs.ErrScreenshot, err.Error()) } if data.B64encoded == "" { return nil, "", errors.Wrap(bmclibErrs.ErrScreenshot, "no screencapture data in response") } image, err = base64.StdEncoding.DecodeString(data.B64encoded) if err != nil { return nil, "", errors.Wrap(bmclibErrs.ErrScreenshot, err.Error()) } return image, fileType, nil } ================================================ FILE: providers/dell/idrac_test.go ================================================ package dell import ( "context" "encoding/base64" "errors" "fmt" "io" "log" "net/http" "net/http/httptest" "net/url" "os" "testing" berrors "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" ) const ( fixturesDir = "./fixtures" ) var endpointFunc = func(file string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // expect either GET or Delete methods if r.Method != http.MethodGet && r.Method != http.MethodDelete { w.WriteHeader(http.StatusNotFound) } fixture := fixturesDir + file fh, err := os.Open(fixture) if err != nil { log.Fatal(err) } defer fh.Close() b, err := io.ReadAll(fh) if err != nil { log.Fatal(err) } _, _ = w.Write(b) } } func Test_Screenshot(t *testing.T) { // byte slice instead of a real image img := []byte(`foobar`) // endpoint to handler funcs type handlerFuncMap map[string]func(http.ResponseWriter, *http.Request) testcases := []struct { name string imgbytes []byte handlerFuncMap handlerFuncMap }{ { "happy path", []byte(`foobar`), handlerFuncMap{ // service root "/redfish/v1/": endpointFunc("/serviceroot.json"), "/redfish/v1/Systems": endpointFunc("/systems.json"), "/redfish/v1/Systems/System.Embedded.1": endpointFunc("/systems_embedded.1.json"), // screenshot endpoint redfishV1Prefix + screenshotEndpoint: func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, http.MethodPost) assert.Equal(t, r.Header.Get("Content-Type"), "application/json") b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, []byte(`{"FileType":"ServerScreenShot"}`), b) encoded := base64.RawStdEncoding.EncodeToString(img) respFmtStr := `{"@Message.ExtendedInfo":[{"Message":"Successfully Completed Request","MessageArgs":[],"MessageArgs@odata.count":0,"MessageId":"Base.1.8.Success","RelatedProperties":[],"RelatedProperties@odata.count":0,"Resolution":"None","Severity":"OK"},{"Message":"The Export Server Screen Shot operation successfully exported the server screen shot file.","MessageArgs":[],"MessageArgs@odata.count":0,"MessageId":"IDRAC.2.5.LC080","RelatedProperties":[],"RelatedProperties@odata.count":0,"Resolution":"Download the encoded Base64 format server screen shot file, decode the Base64 file and then save it as a *.png file.","Severity":"Informational"}],"ServerScreenshotFile":"%s"}` _, _ = w.Write([]byte(fmt.Sprintf(respFmtStr, encoded))) }, }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() for endpoint, handler := range tc.handlerFuncMap { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } //os.Setenv("DEBUG_BMCLIB", "true") client := New(parsedURL.Hostname(), "", "", logr.Discard(), WithPort(parsedURL.Port()), WithUseBasicAuth(true)) err = client.Open(context.TODO()) if err != nil { t.Fatal(err) } img, fileType, err := client.Screenshot(context.TODO()) if err != nil { t.Fatal(err) } assert.Equal(t, tc.imgbytes, img) assert.Equal(t, "png", fileType) }) } } func TestOpenErrors(t *testing.T) { tests := map[string]struct { fns map[string]func(http.ResponseWriter, *http.Request) err error }{ "not dell manufacturer": { fns: map[string]func(http.ResponseWriter, *http.Request){ // service root "/redfish/v1/": endpointFunc("/serviceroot.json"), "/redfish/v1/Systems": endpointFunc("/systems.json"), "/redfish/v1/Systems/System.Embedded.1": endpointFunc("/systems_embedded_not_dell.1.json"), }, err: berrors.ErrIncompatibleProvider, }, "manufacturer failure": { fns: map[string]func(http.ResponseWriter, *http.Request){ // service root "/redfish/v1/": endpointFunc("/serviceroot.json"), "/redfish/v1/Systems": endpointFunc("/systems.json"), "/redfish/v1/Systems/System.Embedded.1": endpointFunc("/systems_embedded_no_manufacturer.1.json"), }, err: errManufacturerUnknown, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { mux := http.NewServeMux() handleFunc := tc.fns for endpoint, handler := range handleFunc { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } client := New(parsedURL.Hostname(), "", "", logr.Discard(), WithPort(parsedURL.Port()), WithUseBasicAuth(true)) err = client.Open(context.TODO()) if !errors.Is(err, tc.err) { t.Fatalf("expected %v, got %v", tc.err, err) } client.Close(context.Background()) }) } } ================================================ FILE: providers/homeassistant/homeassistant.go ================================================ package homeassistant import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "time" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" ) const ( // ProviderName for the HomeAssistant implementation. ProviderName = "homeassistant" // ProviderProtocol for the HomeAssistant implementation. ProviderProtocol = "http" ) // Features implemented by the HomeAssistant provider. var Features = registrar.Features{ providers.FeaturePowerSet, providers.FeaturePowerState, providers.FeatureBootDeviceSet, // no-op } type Config struct { ApiUrl string ApiToken string SwitchEntityID string PowerOperationDelaySeconds uint32 HTTPClient *http.Client Logger logr.Logger } type EntityStateResponse struct { EntityID string FriendlyName string State string } // New returns a new Config containing all the defaults for the HomeAssistant provider. func New(apiUrl string, apiToken string) *Config { return &Config{ ApiUrl: apiUrl, ApiToken: apiToken, HTTPClient: httpclient.Build(), Logger: logr.Discard(), } } // Name returns the name of this HomeAssistant provider. // Implements bmc.Provider interface func (p *Config) Name() string { return ProviderName } // Open a connection to Home Assistant, and validate the entity referenced exists. func (p *Config) Open(ctx context.Context) error { p.Logger.Info("homeassistant provider opened") entityState, err := p.haGetEntityState(ctx, p.SwitchEntityID) if err != nil { return fmt.Errorf("failed to get Home Assistant entity state: %w", err) } p.Logger.Info("Home Assistant entity state", "entity", p.SwitchEntityID, "entityState", entityState) return nil } func (p *Config) haGetEntityState(ctx context.Context, haEntityId string) (EntityStateResponse, error) { stateUrl, err := url.JoinPath(p.ApiUrl, "api", "states", haEntityId) if err != nil { return EntityStateResponse{}, err } p.Logger.Info("Testing connection to Home Assistant API", "url", stateUrl) req, err := http.NewRequestWithContext(ctx, "GET", stateUrl, nil) if err != nil { return EntityStateResponse{}, err } req.Header.Set("Authorization", "Bearer "+p.ApiToken) req.Header.Set("Accept-Encoding", "application/json") resp, err := p.HTTPClient.Do(req) if err != nil { return EntityStateResponse{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return EntityStateResponse{}, fmt.Errorf("failed to connect to Home Assistant API, status code: %d", resp.StatusCode) } if resp.ContentLength < 0 { return EntityStateResponse{}, fmt.Errorf("invalid content length in response: %d", resp.ContentLength) } respBuf := new(bytes.Buffer) if _, err := io.CopyN(respBuf, resp.Body, resp.ContentLength); err != nil { return EntityStateResponse{}, fmt.Errorf("failed to read response body: %w", err) } p.Logger.Info("Successfully connected to Home Assistant API", "entity", haEntityId, "statusCode", resp.StatusCode, "respBuf", respBuf) // Deserialize into a temp struct stateResponse := struct { State string `json:"state"` EntityID string `json:"entity_id"` Attributes map[string]string `json:"attributes"` }{} if err := json.Unmarshal(respBuf.Bytes(), &stateResponse); err != nil { return EntityStateResponse{}, fmt.Errorf("failed to unmarshal response body: %w", err) } // Ensure we have Attributes["friendly_name"] field if _, ok := stateResponse.Attributes["friendly_name"]; !ok { return EntityStateResponse{}, fmt.Errorf("missing friendly_name attribute in response") } finalResponse := EntityStateResponse{ EntityID: stateResponse.EntityID, FriendlyName: stateResponse.Attributes["friendly_name"], State: stateResponse.State, } return finalResponse, nil } // Close a connection to the HomeAssistant consumer. func (p *Config) Close(_ context.Context) (err error) { return nil } // PowerStateGet gets the power state of a BMC machine. func (p *Config) PowerStateGet(ctx context.Context) (state string, err error) { entityState, err := p.haGetEntityState(ctx, p.SwitchEntityID) if err != nil { return "unknown", fmt.Errorf("failed to get Home Assistant entity state: %w", err) } p.Logger.Info("Home Assistant PowerStateGet", "entity", p.SwitchEntityID, "entityState", entityState) return entityState.State, nil } // PowerSet sets the power state of a BMC machine. func (p *Config) PowerSet(ctx context.Context, state string) (ok bool, err error) { // Send a POST request to the Home Assistant API to toggle the switch entity var service string if state == "on" { service = "turn_on" } else if state == "off" { service = "turn_off" } else { return false, fmt.Errorf("invalid power state: %s", state) } serviceUrl, err := url.JoinPath(p.ApiUrl, "api", "services", "switch", service) if err != nil { return false, err } p.Logger.Info("Setting Home Assistant entity power state", "url", serviceUrl, "entity", p.SwitchEntityID, "desiredState", state) reqBodyMap := map[string]interface{}{ "entity_id": p.SwitchEntityID, } reqBodyBytes, err := json.Marshal(reqBodyMap) if err != nil { return false, fmt.Errorf("failed to marshal request body: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", serviceUrl, bytes.NewBuffer(reqBodyBytes)) if err != nil { return false, err } req.Header.Set("Authorization", "Bearer "+p.ApiToken) req.Header.Set("Accept-Encoding", "application/json") req.Header.Set("Content-Type", "application/json") resp, err := p.HTTPClient.Do(req) if err != nil { return false, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted { return false, fmt.Errorf("failed to set power state, status code: %d", resp.StatusCode) } p.Logger.Info("Successfully set Home Assistant entity power state", "entity", p.SwitchEntityID, "desiredState", state) // Sleep for the configured delay to allow the power operation to take effect if p.PowerOperationDelaySeconds > 0 { p.Logger.Info("Waiting for power operation delay", "seconds", p.PowerOperationDelaySeconds) select { case <-ctx.Done(): return false, ctx.Err() case <-time.After(time.Duration(p.PowerOperationDelaySeconds) * time.Second): } p.Logger.Info("Power operation delay complete") } else { p.Logger.Info("No power operation delay configured, proceeding immediately") } return true, nil } // BootDeviceSet is a no-op here. func (p *Config) BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { // fully no-op for now; in the future, some other switch could touch some GPIO which could work with a custom bootloader p.Logger.Info("BootDeviceSet is not implemented for Home Assistant provider; no operation performed", "bootDevice", bootDevice, "setPersistent", setPersistent, "efiBoot", efiBoot) return true, nil } ================================================ FILE: providers/intelamt/intelamt.go ================================================ package intelamt import ( "context" "errors" "strings" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/go-logr/logr" "github.com/jacobweinstock/iamt" "github.com/jacobweinstock/registrar" ) const ( // ProviderName for the provider AMT implementation ProviderName = "IntelAMT" // ProviderProtocol for the provider AMT implementation ProviderProtocol = "AMT" ) var ( // Features implemented by the AMT provider Features = registrar.Features{ providers.FeaturePowerSet, providers.FeaturePowerState, providers.FeatureBootDeviceSet, } ) // iamtClient interface allows us to mock the client for testing type iamtClient interface { Close(context.Context) error IsPoweredOn(context.Context) (bool, error) Open(context.Context) error PowerCycle(context.Context) error PowerOff(context.Context) error PowerOn(context.Context) error SetPXE(context.Context) error } // Conn is a connection to a BMC via Intel AMT type Conn struct { client iamtClient } // Option for setting optional Client values type Option func(*Config) func WithPort(port uint32) Option { return func(c *Config) { c.Port = port } } func WithHostScheme(hostScheme string) Option { return func(c *Config) { c.HostScheme = hostScheme } } func WithLogger(logger logr.Logger) Option { return func(c *Config) { c.Logger = logger } } type Config struct { // HostScheme should be either "http" or "https". HostScheme string // Port is the port number to connect to. Port uint32 Logger logr.Logger } // New creates a new AMT connection func New(host string, user string, pass string, opts ...Option) *Conn { defaultClient := &Config{ HostScheme: "http", Port: 16992, Logger: logr.Discard(), } for _, opt := range opts { opt(defaultClient) } iopts := []iamt.Option{ iamt.WithLogger(defaultClient.Logger), iamt.WithPort(defaultClient.Port), iamt.WithScheme(defaultClient.HostScheme), } return &Conn{ client: iamt.NewClient(host, user, pass, iopts...), } } // Name of the provider func (c *Conn) Name() string { return ProviderName } // Open a connection to the BMC via Intel AMT. func (c *Conn) Open(ctx context.Context) (err error) { return c.client.Open(ctx) } // Close a connection to a BMC func (c *Conn) Close(ctx context.Context) (err error) { return c.client.Close(ctx) } // Compatible tests whether a BMC is compatible with the ipmitool provider func (c *Conn) Compatible(ctx context.Context) bool { if err := c.client.Open(ctx); err != nil { return false } if _, err := c.client.IsPoweredOn(ctx); err != nil { return false } return true } // BootDeviceSet sets the next boot device with options func (c *Conn) BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { if strings.ToLower(bootDevice) != "pxe" { return false, errors.New("only pxe boot device is supported for AMT provider") } if err := c.client.SetPXE(ctx); err != nil { return false, err } return true, nil } // PowerStateGet gets the power state of a BMC machine func (c *Conn) PowerStateGet(ctx context.Context) (state string, err error) { on, err := c.client.IsPoweredOn(ctx) if err != nil { return "", err } if on { return "on", nil } return "off", nil } // PowerSet sets the power state of a BMC machine func (c *Conn) PowerSet(ctx context.Context, state string) (ok bool, err error) { on, _ := c.client.IsPoweredOn(ctx) switch strings.ToLower(state) { case "on": if on { return true, nil } if err := c.client.PowerOn(ctx); err != nil { return false, err } ok = true case "off": if !on { return true, nil } if err := c.client.PowerOff(ctx); err != nil { return false, err } ok = true case "cycle": if err := c.client.PowerCycle(ctx); err != nil { return false, err } ok = true default: err = errors.New("requested state type unknown") } return ok, err } ================================================ FILE: providers/intelamt/intelamt_test.go ================================================ package intelamt import ( "context" "errors" "testing" "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) type mock struct { errSetPXE error errIsPoweredOn error poweredON bool errPowerOn error errPowerOff error errPowerCycle error errOpen error } func (m *mock) Open(ctx context.Context) error { return m.errOpen } func (m *mock) Close(ctx context.Context) error { return nil } func (m *mock) IsPoweredOn(ctx context.Context) (bool, error) { if m.errIsPoweredOn != nil { return false, m.errIsPoweredOn } return m.poweredON, nil } func (m *mock) PowerOn(ctx context.Context) error { return m.errPowerOn } func (m *mock) PowerOff(ctx context.Context) error { return m.errPowerOff } func (m *mock) PowerCycle(ctx context.Context) error { return m.errPowerCycle } func (m *mock) SetPXE(ctx context.Context) error { return m.errSetPXE } func TestClose(t *testing.T) { conn := &Conn{client: &mock{}} if err := conn.Close(context.Background()); err != nil { t.Fatal(err) } } func TestName(t *testing.T) { conn := &Conn{client: &mock{}} if diff := cmp.Diff(conn.Name(), ProviderName); diff != "" { t.Fatal(diff) } } func TestBootDeviceSet(t *testing.T) { tests := map[string]struct { want bool err error failCall bool device string }{ "success": {want: true, device: "pxe"}, "invalid boot device": {want: false, err: errors.New("only pxe boot device is supported for AMT provider"), device: "invalid"}, "failed to set boot device": {want: false, failCall: true, err: errors.New(""), device: "pxe"}, } for name, tt := range tests { t.Run(name, func(t *testing.T) { m := &mock{} if tt.failCall { m = &mock{errSetPXE: tt.err} } conn := &Conn{client: m} ctx := context.Background() if err := conn.Open(ctx); err != nil { t.Fatal(err) } defer conn.Close(ctx) got, err := conn.BootDeviceSet(ctx, tt.device, false, false) if err != nil && tt.err == nil { t.Fatalf("expected nil error, got: %v", err) } if diff := cmp.Diff(got, tt.want); diff != "" { t.Fatal(diff) } }) } } func TestPowerStateGet(t *testing.T) { tests := map[string]struct { want string err error }{ "power on": {want: "on"}, "power off": {want: "off"}, "invalid power state": {want: "", err: errors.New("invalid power state: invalid")}, "failed to set power state": {want: "", err: errors.New("")}, } for name, tt := range tests { t.Run(name, func(t *testing.T) { var state bool switch tt.want { case "on": state = true case "off": state = false default: } m := &mock{poweredON: state, errIsPoweredOn: tt.err} conn := &Conn{client: m} ctx := context.Background() if err := conn.Open(ctx); err != nil { t.Fatal(err) } defer conn.Close(ctx) got, err := conn.PowerStateGet(ctx) if err != nil && tt.err == nil { t.Fatalf("expected nil error, got: %v", err) } if diff := cmp.Diff(got, tt.want); diff != "" { t.Fatal(diff) } }) } } func TestPowerSet(t *testing.T) { tests := map[string]struct { want bool err error poweredOn bool wantState string }{ "power on success": {want: true, wantState: "on"}, "power on success 2": {want: true, wantState: "on", poweredOn: true}, "power on failed": {want: false, wantState: "on", err: errors.New("failed to power on")}, "power off success": {want: true, wantState: "off"}, "power off success 2": {want: true, wantState: "off", poweredOn: true}, "power off failed": {want: false, poweredOn: true, wantState: "off", err: errors.New("failed to power off")}, "power cycle success": {want: true, wantState: "cycle"}, "power cycle failed": {want: false, wantState: "cycle", err: errors.New("failed to power cycle")}, "power cycle failed 2": {want: false, wantState: "cycle", poweredOn: false, err: errors.New("failed to power cycle")}, "invalid power state": {want: false, wantState: "unknown", err: errors.New("requested state type unknown")}, } for name, tt := range tests { t.Run(name, func(t *testing.T) { m := &mock{} switch name { case "power on failed": m.errPowerOn = tt.err case "power off failed": m.errPowerOff = tt.err case "power cycle failed": m.errPowerCycle = tt.err case "power cycle failed 2": m.errPowerCycle = tt.err m.errPowerOn = tt.err default: } m.poweredON = tt.poweredOn conn := &Conn{client: m} ctx := context.Background() if err := conn.Open(ctx); err != nil { t.Fatal(err) } defer conn.Close(ctx) got, err := conn.PowerSet(ctx, tt.wantState) if err != nil && tt.err == nil { t.Fatalf("expected nil error, got: %v", err) } if diff := cmp.Diff(got, tt.want); diff != "" { t.Fatal(diff) } }) } } func TestCompatible(t *testing.T) { tests := map[string]struct { want bool failOnOpen bool }{ "success": {want: true}, "failed on open": {want: false, failOnOpen: true}, "failed on power": {want: false}, } for name, tt := range tests { t.Run(name, func(t *testing.T) { m := &mock{} if !tt.want { if tt.failOnOpen { m.errOpen = errors.New("failed to open") } else { m.errIsPoweredOn = errors.New("failed to power on") } } conn := &Conn{client: m} ctx := context.Background() defer conn.Close(ctx) got := conn.Compatible(ctx) if diff := cmp.Diff(got, tt.want); diff != "" { t.Fatal(diff) } }) } } func TestNew(t *testing.T) { wantClient := &mock{} want := &Conn{client: wantClient} got := New("localhost", "admin", "pass") t.Log(got == nil) c := Conn{} l := logr.Logger{} if diff := cmp.Diff(got, want, cmpopts.IgnoreUnexported(c, l)); diff != "" { t.Fatal(diff) } } ================================================ FILE: providers/ipmitool/ipmitool.go ================================================ package ipmitool import ( "context" "errors" "strings" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/bmc-toolbox/bmclib/v2/internal/ipmi" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" ) const ( // ProviderName for the provider implementation ProviderName = "ipmitool" // ProviderProtocol for the provider implementation ProviderProtocol = "ipmi" ) var ( // Features implemented by ipmitool Features = registrar.Features{ providers.FeaturePowerSet, providers.FeaturePowerState, providers.FeatureUserRead, providers.FeatureBmcReset, providers.FeatureBootDeviceSet, providers.FeatureClearSystemEventLog, providers.FeatureGetSystemEventLog, providers.FeatureGetSystemEventLogRaw, providers.FeatureDeactivateSOL, } ) // Conn for Ipmitool connection details type Conn struct { ipmitool *ipmi.Ipmi log logr.Logger } type Config struct { CipherSuite string IpmitoolPath string Log logr.Logger Port string } // Option for setting optional Client values type Option func(*Config) func WithLogger(log logr.Logger) Option { return func(c *Config) { c.Log = log } } func WithPort(port string) Option { return func(c *Config) { c.Port = port } } func WithCipherSuite(cipherSuite string) Option { return func(c *Config) { c.CipherSuite = cipherSuite } } func WithIpmitoolPath(ipmitoolPath string) Option { return func(c *Config) { c.IpmitoolPath = ipmitoolPath } } func New(host, user, pass string, opts ...Option) (*Conn, error) { defaultConfig := &Config{ Port: "623", Log: logr.Discard(), } for _, opt := range opts { opt(defaultConfig) } iopts := []ipmi.Option{ ipmi.WithIpmitoolPath(defaultConfig.IpmitoolPath), ipmi.WithCipherSuite(defaultConfig.CipherSuite), ipmi.WithLogger(defaultConfig.Log), } ipt, err := ipmi.New(user, pass, host+":"+defaultConfig.Port, iopts...) if err != nil { return nil, err } return &Conn{ipmitool: ipt, log: defaultConfig.Log}, nil } // Open a connection to a BMC func (c *Conn) Open(ctx context.Context) (err error) { _, err = c.ipmitool.PowerState(ctx) if err != nil { return err } return nil } // Close a connection to a BMC func (c *Conn) Close(ctx context.Context) (err error) { return nil } // Compatible tests whether a BMC is compatible with the ipmitool provider func (c *Conn) Compatible(ctx context.Context) bool { err := c.Open(ctx) if err != nil { c.log.V(2).WithValues( "provider", c.Name(), ).Info("warn", bmclibErrs.ErrCompatibilityCheck.Error(), err.Error()) return false } defer c.Close(ctx) _, err = c.ipmitool.PowerState(ctx) if err != nil { c.log.V(2).WithValues( "provider", c.Name(), ).Info("warn", bmclibErrs.ErrCompatibilityCheck.Error(), err.Error()) } return err == nil } func (c *Conn) Name() string { return ProviderName } // BootDeviceSet sets the next boot device with options func (c *Conn) BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { return c.ipmitool.BootDeviceSet(ctx, bootDevice, setPersistent, efiBoot) } // BmcReset will reset a BMC func (c *Conn) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { return c.ipmitool.PowerResetBmc(ctx, resetType) } // DeactivateSOL will deactivate active SOL sessions func (c *Conn) DeactivateSOL(ctx context.Context) (err error) { return c.ipmitool.DeactivateSOL(ctx) } // UserRead list all users func (c *Conn) UserRead(ctx context.Context) (users []map[string]string, err error) { return c.ipmitool.ReadUsers(ctx) } // PowerStateGet gets the power state of a BMC machine func (c *Conn) PowerStateGet(ctx context.Context) (state string, err error) { return c.ipmitool.PowerState(ctx) } // PowerSet sets the power state of a BMC machine func (c *Conn) PowerSet(ctx context.Context, state string) (ok bool, err error) { switch strings.ToLower(state) { case "on": on, errOn := c.ipmitool.IsOn(ctx) if errOn != nil || !on { ok, err = c.ipmitool.PowerOn(ctx) } else { ok = true } case "off": ok, err = c.ipmitool.PowerOff(ctx) case "soft": ok, err = c.ipmitool.PowerSoft(ctx) case "reset": ok, err = c.ipmitool.PowerReset(ctx) case "cycle": ok, err = c.ipmitool.PowerCycle(ctx) default: err = errors.New("requested state type unknown") } return ok, err } func (c *Conn) ClearSystemEventLog(ctx context.Context) (err error) { return c.ipmitool.ClearSystemEventLog(ctx) } func (c *Conn) GetSystemEventLog(ctx context.Context) (entries [][]string, err error) { return c.ipmitool.GetSystemEventLog(ctx) } func (c *Conn) GetSystemEventLogRaw(ctx context.Context) (eventlog string, err error) { return c.ipmitool.GetSystemEventLogRaw(ctx) } // SendNMI tells the BMC to issue an NMI to the device func (c *Conn) SendNMI(ctx context.Context) error { return c.ipmitool.SendPowerDiag(ctx) } ================================================ FILE: providers/ipmitool/ipmitool_test.go ================================================ package ipmitool import ( "context" "fmt" "os" "os/exec" "path/filepath" "testing" "github.com/bmc-toolbox/bmclib/v2/logging" ) func TestMain(m *testing.M) { var tempDir string _, err := exec.LookPath("ipmitool") if err != nil { tempDir, err = os.MkdirTemp("/tmp", "") if err != nil { os.Exit(2) } path := os.Getenv("PATH") + ":" + tempDir os.Setenv("PATH", path) fmt.Println(os.Getenv("PATH")) f := filepath.Join(tempDir, "ipmitool") err = os.WriteFile(f, []byte{}, 0755) if err != nil { os.RemoveAll(tempDir) os.Exit(3) } } code := m.Run() os.RemoveAll(tempDir) os.Exit(code) } func TestPowerState(t *testing.T) { t.Skip("need real ipmi server") user := "ADMIN" pass := "ADMIN" host := "127.0.0.1" port := "623" c, err := New(host, user, pass, WithPort(port), WithLogger(logging.DefaultLogger())) if err != nil { t.Fatal(err) } state, err := c.PowerStateGet(context.Background()) if err != nil { t.Fatal(err) } t.Log(state) t.Fatal() } func TestPowerSet1(t *testing.T) { t.Skip("need real ipmi server") user := "ADMIN" pass := "ADMIN" host := "127.0.0.1" port := "623" c, err := New(host, user, pass, WithPort(port), WithLogger(logging.DefaultLogger())) if err != nil { t.Fatal(err) } state, err := c.PowerSet(context.Background(), "soft") if err != nil { t.Fatal(err) } t.Log(state) t.Fatal() } func TestBootDeviceSet2(t *testing.T) { t.Skip("need real ipmi server") host := "127.0.0.1" port := "623" user := "ADMIN" pass := "ADMIN" i, err := New(host, user, pass, WithPort(port), WithLogger(logging.DefaultLogger())) if err != nil { t.Fatal(err) } state, err := i.BootDeviceSet(context.Background(), "disk", false, false) if err != nil { t.Fatal(err) } t.Log(state) t.Fatal() } func TestBMCReset(t *testing.T) { t.Skip("need real ipmi server") host := "127.0.0.1" port := "623" user := "ADMIN" pass := "ADMIN" i, err := New(host, user, pass, WithPort(port), WithLogger(logging.DefaultLogger())) if err != nil { t.Fatal(err) } state, err := i.BmcReset(context.Background(), "warm") if err != nil { t.Fatal(err) } t.Log(state) t.Fatal() } func TestDeactivateSOL(t *testing.T) { t.Skip("need real ipmi server") host := "127.0.0.1" port := "623" user := "ADMIN" pass := "ADMIN" i, err := New(host, user, pass, WithPort(port), WithLogger(logging.DefaultLogger())) if err != nil { t.Fatal(err) } err = i.DeactivateSOL(context.Background()) if err != nil { t.Fatal(err) } t.Log(err != nil) t.Fatal() } func TestSystemEventLogClear(t *testing.T) { t.Skip("need real ipmi server") host := "127.0.0.1" port := "623" user := "ADMIN" pass := "ADMIN" i, err := New(host, user, pass, WithPort(port), WithLogger(logging.DefaultLogger())) if err != nil { t.Fatal(err) } err = i.ClearSystemEventLog(context.Background()) if err != nil { t.Fatal(err) } t.Log("System Event Log cleared") t.Fatal() } func TestSystemEventLogGet(t *testing.T) { t.Skip("need real ipmi server") host := "127.0.0.1" port := "623" user := "ADMIN" pass := "ADMIN" i, err := New(host, user, pass, WithPort(port), WithLogger(logging.DefaultLogger())) if err != nil { t.Fatal(err) } entries, err := i.GetSystemEventLog(context.Background()) if err != nil { t.Fatal(err) } t.Log(entries) t.Fatal() } func TestSystemEventLogGetRaw(t *testing.T) { t.Skip("need real ipmi server") host := "127.0.0.1" port := "623" user := "ADMIN" pass := "ADMIN" i, err := New(host, user, pass, WithPort(port), WithLogger(logging.DefaultLogger())) if err != nil { t.Fatal(err) } eventlog, err := i.GetSystemEventLogRaw(context.Background()) if err != nil { t.Fatal(err) } t.Log(eventlog) t.Fatal() } func TestSendNMI(t *testing.T) { t.Skip("need real ipmi server") host := "127.0.0.1" port := "623" user := "ADMIN" pass := "ADMIN" i, err := New(host, user, pass, WithPort(port), WithLogger(logging.DefaultLogger())) if err != nil { t.Fatal(err) } err = i.SendNMI(context.Background()) if err != nil { t.Fatal(err) } t.Log("NMI sent") t.Fatal() } ================================================ FILE: providers/openbmc/firmware.go ================================================ package openbmc import ( "context" "fmt" "os" "strings" "time" "github.com/bmc-toolbox/bmclib/v2/constants" "github.com/bmc-toolbox/common" bmcliberrs "github.com/bmc-toolbox/bmclib/v2/errors" rfw "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" "github.com/pkg/errors" "github.com/stmcginnis/gofish/schemas" ) // bmc client interface implementations methods func (c *Conn) FirmwareInstallSteps(ctx context.Context, component string) ([]constants.FirmwareInstallStep, error) { if err := c.deviceSupported(ctx); err != nil { return nil, err } switch strings.ToUpper(component) { case common.SlugBIOS: return []constants.FirmwareInstallStep{ constants.FirmwareInstallStepPowerOffHost, constants.FirmwareInstallStepUploadInitiateInstall, constants.FirmwareInstallStepInstallStatus, }, nil case common.SlugBMC: return []constants.FirmwareInstallStep{ constants.FirmwareInstallStepUploadInitiateInstall, constants.FirmwareInstallStepInstallStatus, }, nil default: return nil, errors.New("component firmware install not supported: " + component) } } func (c *Conn) FirmwareInstallUploadAndInitiate(ctx context.Context, component string, file *os.File) (taskID string, err error) { if err := c.deviceSupported(ctx); err != nil { return "", errNotOpenBMCDevice } // // expect atleast 5 minutes left in the deadline to proceed with the upload d, _ := ctx.Deadline() if time.Until(d) < 10*time.Minute { return "", errors.New("remaining context deadline insufficient to perform update: " + time.Until(d).String()) } // list current tasks on BMC tasks, err := c.redfishwrapper.Tasks(ctx) if err != nil { return "", errors.Wrap(err, "error listing bmc redfish tasks") } // validate a new firmware install task can be queued if err := c.checkQueueability(component, tasks); err != nil { return "", errors.Wrap(bmcliberrs.ErrFirmwareInstall, err.Error()) } params := &rfw.RedfishUpdateServiceParameters{ Targets: []string{}, OperationApplyTime: constants.OnReset, Oem: []byte(`{}`), } return c.redfishwrapper.FirmwareUpload(ctx, file, params) } // returns an error when a bmc firmware install is active func (c *Conn) checkQueueability(component string, tasks []*schemas.Task) error { errTaskActive := errors.New("A firmware job was found active for component: " + component) for _, t := range tasks { // taskInfo returned in error if any. taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", t.ID, t.TaskState, t.TaskStatus) // convert redfish task state to bmclib state convstate := c.redfishwrapper.ConvertTaskState(string(t.TaskState)) // check if task is active based on converted state active, err := c.redfishwrapper.TaskStateActive(convstate) if err != nil { return errors.Wrap(err, taskInfo) } if active { return errors.Wrap(errTaskActive, taskInfo) } } return nil } // FirmwareTaskStatus returns the status of a firmware related task queued on the BMC. func (c *Conn) FirmwareTaskStatus(ctx context.Context, kind constants.FirmwareInstallStep, component, taskID, installVersion string) (state constants.TaskState, status string, err error) { return c.redfishwrapper.TaskStatus(ctx, taskID) } ================================================ FILE: providers/openbmc/openbmc.go ================================================ package openbmc import ( "bytes" "context" "crypto/x509" "io" "net/http" "strings" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/bmc-toolbox/common" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" "github.com/pkg/errors" ) const ( // ProviderName for the OpenBMC provider implementation ProviderName = "openbmc" // ProviderProtocol for the OpenBMC provider implementation ProviderProtocol = "redfish" ) var ( // Features implemented by dell redfish Features = registrar.Features{ providers.FeaturePowerState, providers.FeaturePowerSet, providers.FeatureBmcReset, providers.FeatureFirmwareInstallSteps, providers.FeatureFirmwareUploadInitiateInstall, providers.FeatureFirmwareTaskStatus, providers.FeatureInventoryRead, } errNotOpenBMCDevice = errors.New("not an OpenBMC device") ) type Config struct { HttpClient *http.Client Port string VersionsNotCompatible []string RootCAs *x509.CertPool UseBasicAuth bool } // Option for setting optional Client values type Option func(*Config) func WithHttpClient(httpClient *http.Client) Option { return func(c *Config) { c.HttpClient = httpClient } } func WithPort(port string) Option { return func(c *Config) { c.Port = port } } func WithRootCAs(rootCAs *x509.CertPool) Option { return func(c *Config) { c.RootCAs = rootCAs } } func WithUseBasicAuth(useBasicAuth bool) Option { return func(c *Config) { c.UseBasicAuth = useBasicAuth } } // Conn details for redfish client type Conn struct { host string httpClient *http.Client redfishwrapper *redfishwrapper.Client Log logr.Logger } // New returns connection with a redfish client initialized func New(host, user, pass string, log logr.Logger, opts ...Option) *Conn { defaultConfig := &Config{ HttpClient: httpclient.Build(), Port: "443", VersionsNotCompatible: []string{}, } for _, opt := range opts { opt(defaultConfig) } rfOpts := []redfishwrapper.Option{ redfishwrapper.WithHTTPClient(defaultConfig.HttpClient), redfishwrapper.WithBasicAuthEnabled(defaultConfig.UseBasicAuth), redfishwrapper.WithEtagMatchDisabled(true), } if defaultConfig.RootCAs != nil { rfOpts = append(rfOpts, redfishwrapper.WithSecureTLS(defaultConfig.RootCAs)) } return &Conn{ host: host, httpClient: defaultConfig.HttpClient, Log: log, redfishwrapper: redfishwrapper.NewClient(host, defaultConfig.Port, user, pass, rfOpts...), } } // Open a connection to a BMC via redfish func (c *Conn) Open(ctx context.Context) (err error) { if err := c.deviceSupported(ctx); err != nil { return err } if err := c.redfishwrapper.Open(ctx); err != nil { return err } return nil } func (c *Conn) deviceSupported(ctx context.Context) error { var host = c.host if !strings.HasPrefix(host, "https://") && !strings.HasPrefix(host, "http://") { host = "https://" + host } req, err := http.NewRequestWithContext(ctx, http.MethodGet, host, nil) if err != nil { return err } resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return err } if !bytes.Contains(b, []byte(`OpenBMC`)) { return errNotOpenBMCDevice } return nil } // Close a connection to a BMC via redfish func (c *Conn) Close(ctx context.Context) error { return c.redfishwrapper.Close(ctx) } // Name returns the client provider name. func (c *Conn) Name() string { return ProviderName } // PowerStateGet gets the power state of a BMC machine func (c *Conn) PowerStateGet(ctx context.Context) (state string, err error) { return c.redfishwrapper.SystemPowerStatus(ctx) } // PowerSet sets the power state of a server func (c *Conn) PowerSet(ctx context.Context, state string) (ok bool, err error) { return c.redfishwrapper.PowerSet(ctx, state) } // Inventory collects hardware inventory and install firmware information func (c *Conn) Inventory(ctx context.Context) (device *common.Device, err error) { return c.redfishwrapper.Inventory(ctx, false) } // BmcReset power cycles the BMC func (c *Conn) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { return c.redfishwrapper.BMCReset(ctx, resetType) } // SendNMI tells the BMC to issue an NMI to the device func (c *Conn) SendNMI(ctx context.Context) error { return c.redfishwrapper.SendNMI(ctx) } ================================================ FILE: providers/providers.go ================================================ package providers import "github.com/jacobweinstock/registrar" const ( // FeaturePowerState represents the powerstate functionality // an implementation will use these when they have implemented // the corresponding interface method. FeaturePowerState registrar.Feature = "powerstate" // FeaturePowerSet means an implementation can set a BMC power state FeaturePowerSet registrar.Feature = "powerset" // FeatureUserCreate means an implementation can create BMC users FeatureUserCreate registrar.Feature = "usercreate" // FeatureUserDelete means an implementation can delete BMC users FeatureUserDelete registrar.Feature = "userdelete" // FeatureUserUpdate means an implementation can update BMC users FeatureUserUpdate registrar.Feature = "userupdate" // FeatureUserRead means an implementation can read BMC users FeatureUserRead registrar.Feature = "userread" // FeatureBmcReset means an implementation can warm or cold reset a BMC FeatureBmcReset registrar.Feature = "bmcreset" // FeatureBootDeviceSet means an implementation the next boot device FeatureBootDeviceSet registrar.Feature = "bootdeviceset" // FeaturesVirtualMedia means an implementation can manage virtual media devices FeatureVirtualMedia registrar.Feature = "virtualmedia" // FeatureMountFloppyImage means an implementation uploads a floppy image for mounting as virtual media. // // note: This is differs from FeatureVirtualMedia which is limited to accepting a URL to download the image from. FeatureMountFloppyImage registrar.Feature = "mountFloppyImage" // FeatureUnmountFloppyImage means an implementation removes a floppy image that was previously uploaded. FeatureUnmountFloppyImage registrar.Feature = "unmountFloppyImage" // FeatureFirmwareInstall means an implementation that initiates the firmware install process // FeatureFirmwareInstall means an implementation that uploads _and_ initiates the firmware install process FeatureFirmwareInstall registrar.Feature = "firmwareinstall" // FeatureFirmwareInstallSatus means an implementation that returns the firmware install status FeatureFirmwareInstallStatus registrar.Feature = "firmwareinstallstatus" // FeatureInventoryRead means an implementation that returns the hardware and firmware inventory FeatureInventoryRead registrar.Feature = "inventoryread" // FeaturePostCodeRead means an implementation that returns the boot BIOS/UEFI post code status and value FeaturePostCodeRead registrar.Feature = "postcoderead" // FeatureScreenshot means an implementation that returns a screenshot of the video. FeatureScreenshot registrar.Feature = "screenshot" // FeatureClearSystemEventLog means an implementation that clears the BMC System Event Log (SEL) FeatureClearSystemEventLog registrar.Feature = "clearsystemeventlog" // FeatureGetSystemEventLog means an implementation that returns the BMC System Event Log (SEL) FeatureGetSystemEventLog registrar.Feature = "getsystemeventlog" // FeatureGetSystemEventLogRaw means an implementation that returns the BMC System Event Log (SEL) in raw format FeatureGetSystemEventLogRaw registrar.Feature = "getsystemeventlograw" // FeatureFirmwareInstallSteps means an implementation returns the steps part of the firmware update process. FeatureFirmwareInstallSteps registrar.Feature = "firmwareinstallsteps" // FeatureFirmwareUpload means an implementation that uploads firmware for installing. FeatureFirmwareUpload registrar.Feature = "firmwareupload" // FeatureFirmwareInstallUploaded means an implementation that installs firmware uploaded using the firmwareupload feature. FeatureFirmwareInstallUploaded registrar.Feature = "firmwareinstalluploaded" // FeatureFirmwareTaskStatus identifies an implementaton that can return the status of a firmware upload/install task. FeatureFirmwareTaskStatus registrar.Feature = "firmwaretaskstatus" // FeatureFirmwareUploadInitiateInstall identifies an implementation that uploads firmware _and_ initiates the install process. FeatureFirmwareUploadInitiateInstall registrar.Feature = "uploadandinitiateinstall" // FeatureDeactivateSOL means an implementation that can deactivate active SOL sessions FeatureDeactivateSOL registrar.Feature = "deactivatesol" // FeatureResetBiosConfiguration means an implementation that can reset bios configuration back to 'factory' defaults FeatureResetBiosConfiguration registrar.Feature = "resetbiosconfig" // FeatureSetBiosConfiguration means an implementation that can set bios configuration from an input k/v map FeatureSetBiosConfiguration registrar.Feature = "setbiosconfig" // FeatureSetBiosConfigurationFromFile means an implementation that can set bios configuration from a vendor specific text file FeatureSetBiosConfigurationFromFile registrar.Feature = "setbiosconfigfile" // FeatureGetBiosConfiguration means an implementation that can get bios configuration in a simple k/v map FeatureGetBiosConfiguration registrar.Feature = "getbiosconfig" // FeatureBootProgress indicates that the implementation supports reading the BootProgress from the BMC FeatureBootProgress registrar.Feature = "bootprogress" ) ================================================ FILE: providers/redfish/fixtures/v1/dell/entries.json ================================================ { "@odata.context": "/redfish/v1/$metadata#LogEntryCollection.LogEntryCollection", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Entries", "@odata.type": "#LogEntryCollection.LogEntryCollection", "Description": "System Event Logs for this manager", "Members": [ { "@odata.context": "/redfish/v1/$metadata#LogEntry.LogEntry", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Entries/2", "@odata.type": "#LogEntry.v1_6_1.LogEntry", "Created": "2023-01-01T00:00:00-00:00", "Description": "Log Entry 2", "EntryCode": "Assert", "EntryType": "SEL", "GeneratorId": "0x0001", "Id": "1", "Links": {}, "Message": "OEM software event.", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "d000000", "Name": "Log Entry 2", "SensorNumber": 999, "SensorType": null, "Severity": "OK" }, { "@odata.context": "/redfish/v1/$metadata#LogEntry.LogEntry", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Entries/1", "@odata.type": "#LogEntry.v1_6_1.LogEntry", "Created": "2023-01-01T00:00:00-00:00", "Description": "Log Entry 1", "EntryCode": "Deassert", "EntryType": "SEL", "GeneratorId": "0x0001", "Id": "1", "Links": {}, "Message": "OEM software event.", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "d000000", "Name": "Log Entry 1", "SensorNumber": 999, "SensorType": null, "Severity": "OK" } ], "Members@odata.count": 2, "Name": "Log Entry Collection" } ================================================ FILE: providers/redfish/fixtures/v1/dell/job_delete_ok.json ================================================ { "@Message.ExtendedInfo": [ { "Message": "Successfully Completed Request", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "Base.1.7.Success", "RelatedProperties": [], "RelatedProperties@odata.count": 0, "Resolution": "None", "Severity": "OK" }, { "Message": "The operation successfully completed.", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "IDRAC.2.4.SYS413", "RelatedProperties": [], "RelatedProperties@odata.count": 0, "Resolution": "No response action is required.", "Severity": "Informational" } ] } ================================================ FILE: providers/redfish/fixtures/v1/dell/jobs.json ================================================ { "@odata.context": "/redfish/v1/$metadata#DellJobCollection.DellJobCollection", "@odata.type": "#DellJobCollection.DellJobCollection", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs", "Description": "Collection of Job Instances", "Members": [ { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_134386578592", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-15T19:24:36", "Description": "Job Instance", "EndTime": null, "Id": "JID_134386578592", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-15T19:24:17", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_134561935885", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-16T00:16:54", "Description": "Job Instance", "EndTime": null, "Id": "JID_134561935885", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-16T00:16:33", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_134628507565", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-16T02:07:51", "Description": "Job Instance", "EndTime": null, "Id": "JID_134628507565", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-16T02:07:30", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_134734988942", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-16T05:05:19", "Description": "Job Instance", "EndTime": null, "Id": "JID_134734988942", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-16T05:04:58", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_134769926208", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-16T06:03:33", "Description": "Job Instance", "EndTime": null, "Id": "JID_134769926208", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-16T06:03:12", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135707407125", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-17T08:05:59", "Description": "Job Instance", "EndTime": null, "Id": "JID_135707407125", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-17T08:05:40", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135729671798", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-17T08:43:08", "Description": "Job Instance", "EndTime": null, "Id": "JID_135729671798", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-17T08:42:47", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135762432944", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-17T09:37:42", "Description": "Job Instance", "EndTime": null, "Id": "JID_135762432944", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-17T09:37:23", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135792879555", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-17T10:28:28", "Description": "Job Instance", "EndTime": null, "Id": "JID_135792879555", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-17T10:28:07", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135855732001", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-17T12:13:13", "Description": "Job Instance", "EndTime": null, "Id": "JID_135855732001", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-17T12:12:53", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_135889086333", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-02-17T13:08:46", "Description": "Job Instance", "EndTime": null, "Id": "JID_135889086333", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-02-17T13:08:28", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_155889974199", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-03-12T16:43:38", "Description": "Job Instance", "EndTime": null, "Id": "JID_155889974199", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-03-12T16:43:17", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_157492856325", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-03-14T14:15:04", "Description": "Job Instance", "EndTime": null, "Id": "JID_157492856325", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-03-14T14:14:45", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_157527722202", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-03-14T15:13:12", "Description": "Job Instance", "EndTime": null, "Id": "JID_157527722202", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-03-14T15:12:52", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_157563681404", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-03-14T16:13:08", "Description": "Job Instance", "EndTime": null, "Id": "JID_157563681404", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-03-14T16:12:48", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_157882741266", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-03-15T01:04:54", "Description": "Job Instance", "EndTime": null, "Id": "JID_157882741266", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-03-15T01:04:34", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_157915627166", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-03-15T01:59:43", "Description": "Job Instance", "EndTime": null, "Id": "JID_157915627166", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-03-15T01:59:22", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_158221456256", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-03-15T10:29:25", "Description": "Job Instance", "EndTime": null, "Id": "JID_158221456256", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-03-15T10:29:05", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_158337818297", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-03-15T13:43:22", "Description": "Job Instance", "EndTime": null, "Id": "JID_158337818297", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-03-15T13:43:01", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_158625018022", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-03-15T21:42:02", "Description": "Job Instance", "EndTime": null, "Id": "JID_158625018022", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-03-15T21:41:41", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_291745931343", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-08-16T23:30:13", "Description": "Job Instance", "EndTime": null, "Id": "JID_291745931343", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-08-16T23:29:53", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_348594655738", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-10-21T18:47:46", "Description": "Job Instance", "EndTime": "TIME_NA", "Id": "JID_348594655738", "JobState": "Completed", "JobType": "BIOSConfiguration", "Message": "Job completed successfully.", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "PR19", "Name": "Configure: BIOS.Setup.1-1", "PercentComplete": 100, "StartTime": "2021-10-21T18:37:45", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_348601794602", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2021-10-21T18:49:59", "Description": "Job Instance", "EndTime": null, "Id": "JID_348601794602", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2021-10-21T18:49:39", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_421738664260", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": "2022-01-14T09:24:26", "ActualRunningStopTime": "2022-01-14T09:24:57", "CompletionTime": "2022-01-14T09:24:57", "Description": "Job Instance", "EndTime": null, "Id": "JID_421738664260", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2022-01-14T09:24:26", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_452370412267", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": "2022-02-18T20:17:21", "ActualRunningStopTime": "2022-02-18T20:17:53", "CompletionTime": "2022-02-18T20:17:53", "Description": "Job Instance", "EndTime": null, "Id": "JID_452370412267", "JobState": "Completed", "JobType": "ExportConfiguration", "Message": "Successfully exported Server Configuration Profile", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "SYS043", "Name": "Export: Server Configuration Profile", "PercentComplete": 100, "StartTime": "2022-02-18T20:17:21", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_467762674724", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": null, "Description": "Job Instance", "EndTime": "TIME_NA", "Id": "JID_467762674724", "JobState": "Scheduled", "JobType": "FirmwareUpdate", "Message": "Task successfully scheduled.", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "JCP001", "Name": "Firmware Update: BIOS", "PercentComplete": 0, "StartTime": "2022-03-08T15:51:07", "TargetSettingsURI": null }, { "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_467767920358", "@odata.type": "#DellJob.v1_1_0.DellJob", "ActualRunningStartTime": null, "ActualRunningStopTime": null, "CompletionTime": "2022-03-08T16:02:33", "Description": "Job Instance", "EndTime": null, "Id": "JID_467767920358", "JobState": "Completed", "JobType": "FirmwareUpdate", "Message": "Job completed successfully.", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "RED001", "Name": "Firmware Update: iDRAC with Lifecycle Controller", "PercentComplete": 100, "StartTime": "2022-03-08T15:59:52", "TargetSettingsURI": null } ], "Members@odata.count": 27, "Name": "JobQueue" } ================================================ FILE: providers/redfish/fixtures/v1/dell/logservices.json ================================================ { "@odata.context": "/redfish/v1/$metadata#LogServiceCollection.LogServiceCollection", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices", "@odata.type": "#LogServiceCollection.LogServiceCollection", "Description": "Collection of Log Services for this Manager", "Members": [ { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel" } ], "Members@odata.count": 3, "Name": "Log Service Collection" } ================================================ FILE: providers/redfish/fixtures/v1/dell/logservices.sel.json ================================================ { "@odata.context": "/redfish/v1/$metadata#LogService.LogService", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel", "@odata.type": "#LogService.v1_1_3.LogService", "Actions": { "#LogService.ClearLog": { "target": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Actions/LogService.ClearLog" } }, "DateTime": "2023-01-01T00:00:00-00:00", "DateTimeLocalOffset": "00:00", "Description": "SEL Log Service", "Entries": { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Entries" }, "Id": "Sel", "LogEntryType": "SEL", "MaxNumberOfRecords": 1024, "Name": "SEL Log Service", "OverWritePolicy": "WrapsWhenFull", "ServiceEnabled": true } ================================================ FILE: providers/redfish/fixtures/v1/dell/manager.idrac.embedded.1.json ================================================ { "@odata.context": "/redfish/v1/$metadata#Manager.Manager", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1", "@odata.type": "#Manager.v1_9_0.Manager", "LogServices": { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices" }, "Comment": "This isn't part of the schema - this file contains only the bare minimum to make the schema validator happy" } ================================================ FILE: providers/redfish/fixtures/v1/dell/managers.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ManagerCollection.ManagerCollection", "@odata.id": "/redfish/v1/Managers", "@odata.type": "#ManagerCollection.ManagerCollection", "Description": "BMC", "Members": [ { "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1" } ], "Members@odata.count": 1, "Name": "Manager" } ================================================ FILE: providers/redfish/fixtures/v1/dell/selentries/1.json ================================================ { "@odata.context": "/redfish/v1/$metadata#LogEntry.LogEntry", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Entries/1", "@odata.type": "#LogEntry.v1_6_1.LogEntry", "Created": "2023-01-01T00:00:00-00:00", "Description": "Log Entry 1", "EntryCode": "Deassert", "EntryType": "SEL", "GeneratorId": "0x0001", "Id": "1", "Links": {}, "Message": "OEM software event.", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "d000000", "Name": "Log Entry 1", "SensorNumber": 999, "SensorType": null, "Severity": "OK" } ================================================ FILE: providers/redfish/fixtures/v1/dell/selentries/2.json ================================================ { "@odata.context": "/redfish/v1/$metadata#LogEntry.LogEntry", "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Entries/2", "@odata.type": "#LogEntry.v1_6_1.LogEntry", "Created": "2023-01-01T00:00:00-00:00", "Description": "Log Entry 2", "EntryCode": "Assert", "EntryType": "SEL", "GeneratorId": "0x0001", "Id": "1", "Links": {}, "Message": "OEM software event.", "MessageArgs": [], "MessageArgs@odata.count": 0, "MessageId": "d000000", "Name": "Log Entry 2", "SensorNumber": 999, "SensorType": null, "Severity": "OK" } ================================================ FILE: providers/redfish/fixtures/v1/serviceroot.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ServiceRoot.ServiceRoot", "@odata.id": "/redfish/v1", "@odata.type": "#ServiceRoot.v1_6_0.ServiceRoot", "AccountService": { "@odata.id": "/redfish/v1/AccountService" }, "CertificateService": { "@odata.id": "/redfish/v1/CertificateService" }, "Chassis": { "@odata.id": "/redfish/v1/Chassis" }, "Description": "Root Service", "EventService": { "@odata.id": "/redfish/v1/EventService" }, "Fabrics": { "@odata.id": "/redfish/v1/Fabrics" }, "Id": "RootService", "JobService": { "@odata.id": "/redfish/v1/JobService" }, "JsonSchemas": { "@odata.id": "/redfish/v1/JsonSchemas" }, "Links": { "Sessions": { "@odata.id": "/redfish/v1/SessionService/Sessions" } }, "Managers": { "@odata.id": "/redfish/v1/Managers" }, "Name": "Root Service", "Oem": { "Dell": { "@odata.context": "/redfish/v1/$metadata#DellServiceRoot.DellServiceRoot", "@odata.type": "#DellServiceRoot.v1_0_0.DellServiceRoot", "IsBranded": 0, "ManagerMACAddress": "d0:8e:79:bb:3e:ea", "ServiceTag": "FOOBAR" } }, "Product": "Integrated Dell Remote Access Controller", "ProtocolFeaturesSupported": { "ExcerptQuery": false, "ExpandQuery": { "ExpandAll": true, "Levels": true, "Links": true, "MaxLevels": 1, "NoLinks": true }, "FilterQuery": true, "OnlyMemberQuery": true, "SelectQuery": true }, "RedfishVersion": "1.9.0", "Registries": { "@odata.id": "/redfish/v1/Registries" }, "SessionService": { "@odata.id": "/redfish/v1/SessionService" }, "Systems": { "@odata.id": "/redfish/v1/Systems" }, "Tasks": { "@odata.id": "/redfish/v1/TaskService" }, "TelemetryService": { "@odata.id": "/redfish/v1/TelemetryService" }, "UpdateService": { "@odata.id": "/redfish/v1/UpdateService" }, "Vendor": "Dell" } ================================================ FILE: providers/redfish/fixtures/v1/systems.json ================================================ { "@odata.context": "/redfish/v1/$metadata#ComputerSystemCollection.ComputerSystemCollection", "@odata.id": "/redfish/v1/Systems", "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", "Description": "Collection of Computer Systems", "Members": [ { "@odata.id": "/redfish/v1/Systems/System.Embedded.1" } ], "Members@odata.count": 1, "Name": "Computer System Collection" } ================================================ FILE: providers/redfish/fixtures/v1/updateservice.json ================================================ { "@odata.context": "/redfish/v1/$metadata#UpdateService.UpdateService", "@odata.id": "/redfish/v1/UpdateService", "@odata.type": "#UpdateService.v1_8_0.UpdateService", "Actions": { "#UpdateService.SimpleUpdate": { "@Redfish.OperationApplyTimeSupport": { "@odata.type": "#Settings.v1_3_0.OperationApplyTimeSupport", "SupportedValues": [ "Immediate", "OnReset" ] }, "TransferProtocol@Redfish.AllowableValues": [ "HTTP", "NFS", "CIFS", "TFTP", "HTTPS" ], "target": "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate" }, "Oem": { "DellUpdateService.v1_1_0#DellUpdateService.Install": { "InstallUpon@Redfish.AllowableValues": [ "Now", "NowAndReboot", "NextReboot" ], "target": "/redfish/v1/UpdateService/Actions/Oem/DellUpdateService.Install" } } }, "Description": "Represents the properties for the Update Service", "FirmwareInventory": { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory" }, "HttpPushUri": "/redfish/v1/UpdateService/FirmwareInventory", "Id": "UpdateService", "MaxImageSizeBytes": null, "MultipartHttpPushUri": "/redfish/v1/UpdateService/MultipartUpload", "Name": "Update Service", "ServiceEnabled": true, "SoftwareInventory": { "@odata.id": "/redfish/v1/UpdateService/SoftwareInventory" }, "Status": { "Health": "OK", "State": "Enabled" } } ================================================ FILE: providers/redfish/main_test.go ================================================ package redfish import ( "context" "io" "log" "net/http" "net/http/httptest" "net/url" "os" "testing" "github.com/go-logr/logr" ) const ( fixturesDir = "./fixtures" ) var ( mockServer *httptest.Server mockBMCHost *url.URL mockClient *Conn ) // jsonResponse returns the fixture json response for a request URI func jsonResponse(endpoint string) []byte { jsonResponsesMap := map[string]string{ "/redfish/v1/Managers": fixturesDir + "/v1/dell/managers.json", "/redfish/v1/Managers/iDRAC.Embedded.1": fixturesDir + "/v1/dell/manager.idrac.embedded.1.json", "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices": fixturesDir + "/v1/dell/logservices.json", "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel": fixturesDir + "/v1/dell/logservices.sel.json", "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Entries": fixturesDir + "/v1/dell/entries.json", "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Entries/1": fixturesDir + "/v1/dell/selentries/1.json", "/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Entries/2": fixturesDir + "/v1/dell/selentries/2.json", "/redfish/v1/": fixturesDir + "/v1/serviceroot.json", "/redfish/v1/UpdateService": fixturesDir + "/v1/updateservice.json", "/redfish/v1/Systems": fixturesDir + "/v1/systems.json", } fh, err := os.Open(jsonResponsesMap[endpoint]) if err != nil { log.Fatalf("%s, failed to open fixture: %s for endpoint: %s", err.Error(), jsonResponsesMap[endpoint], endpoint) } defer fh.Close() b, err := io.ReadAll(fh) if err != nil { log.Fatalf("%s, failed to read fixture: %s for endpoint: %s", err.Error(), jsonResponsesMap[endpoint], endpoint) } return b } func TestMain(m *testing.M) { // setup mock server mockServer = func() *httptest.Server { handler := http.NewServeMux() handler.HandleFunc("/redfish/v1/", serviceRoot) handler.HandleFunc("/redfish/v1/SessionService/Sessions", sessionService) return httptest.NewTLSServer(handler) }() mockBMCHost, _ = url.Parse(mockServer.URL) mockClient = New(mockBMCHost.Hostname(), "foo", "bar", logr.Discard(), WithPort(mockBMCHost.Port())) err := mockClient.Open(context.TODO()) if err != nil { log.Fatal(err) } os.Exit(m.Run()) } func serviceRoot(w http.ResponseWriter, r *http.Request) { // expect either GET or Delete methods if r.Method != http.MethodGet && r.Method != http.MethodDelete { w.WriteHeader(http.StatusNotFound) } _, _ = w.Write(jsonResponse(r.RequestURI)) } func sessionService(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusNotFound) } _, err := io.ReadAll(r.Body) if err != nil { log.Fatal(err) } w.Header().Set("X-Auth-Token", "hunter2") w.Header().Set("Location", r.URL.String()) _, _ = w.Write([]byte(`is cool`)) } ================================================ FILE: providers/redfish/redfish.go ================================================ package redfish import ( "context" "crypto/x509" "net/http" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/bmc-toolbox/common" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" "github.com/bmc-toolbox/bmclib/v2/bmc" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" ) const ( // ProviderName for the provider implementation ProviderName = "gofish" // ProviderProtocol for the provider implementation ProviderProtocol = "redfish" ) var ( // Features implemented by gofish Features = registrar.Features{ providers.FeaturePowerSet, providers.FeaturePowerState, providers.FeatureUserCreate, providers.FeatureUserUpdate, providers.FeatureUserDelete, providers.FeatureBootDeviceSet, providers.FeatureVirtualMedia, providers.FeatureInventoryRead, providers.FeatureBmcReset, providers.FeatureClearSystemEventLog, providers.FeatureGetBiosConfiguration, providers.FeatureSetBiosConfiguration, providers.FeatureResetBiosConfiguration, } ) // Conn details for redfish client type Conn struct { redfishwrapper *redfishwrapper.Client failInventoryOnError bool Log logr.Logger } type Config struct { HttpClient *http.Client Port string // VersionsNotCompatible is the list of incompatible redfish versions. // // With this option set, The bmclib.Registry.FilterForCompatible(ctx) method will not proceed on // devices with the given redfish version(s). VersionsNotCompatible []string RootCAs *x509.CertPool UseBasicAuth bool // DisableEtagMatch disables the If-Match Etag header from being included by the Gofish driver. DisableEtagMatch bool SystemName string } // Option for setting optional Client values type Option func(*Config) func WithHttpClient(httpClient *http.Client) Option { return func(c *Config) { c.HttpClient = httpClient } } func WithPort(port string) Option { return func(c *Config) { c.Port = port } } func WithVersionsNotCompatible(versionsNotCompatible []string) Option { return func(c *Config) { c.VersionsNotCompatible = versionsNotCompatible } } func WithRootCAs(rootCAs *x509.CertPool) Option { return func(c *Config) { c.RootCAs = rootCAs } } func WithUseBasicAuth(useBasicAuth bool) Option { return func(c *Config) { c.UseBasicAuth = useBasicAuth } } func WithSystemName(name string) Option { return func(c *Config) { c.SystemName = name } } // WithEtagMatchDisabled disables the If-Match Etag header from being included by the Gofish driver. // // As of the current implementation this disables the header for POST/PATCH requests to the System entity endpoints. func WithEtagMatchDisabled(d bool) Option { return func(c *Config) { c.DisableEtagMatch = d } } // New returns connection with a redfish client initialized func New(host, user, pass string, log logr.Logger, opts ...Option) *Conn { defaultConfig := &Config{ HttpClient: httpclient.Build(), Port: "443", VersionsNotCompatible: []string{}, } for _, opt := range opts { opt(defaultConfig) } rfOpts := []redfishwrapper.Option{ redfishwrapper.WithHTTPClient(defaultConfig.HttpClient), redfishwrapper.WithVersionsNotCompatible(defaultConfig.VersionsNotCompatible), redfishwrapper.WithEtagMatchDisabled(defaultConfig.DisableEtagMatch), redfishwrapper.WithBasicAuthEnabled(defaultConfig.UseBasicAuth), redfishwrapper.WithSystemName(defaultConfig.SystemName), } if defaultConfig.RootCAs != nil { rfOpts = append(rfOpts, redfishwrapper.WithSecureTLS(defaultConfig.RootCAs)) } return &Conn{ Log: log, failInventoryOnError: false, redfishwrapper: redfishwrapper.NewClient(host, defaultConfig.Port, user, pass, rfOpts...), } } // Open a connection to a BMC via redfish func (c *Conn) Open(ctx context.Context) (err error) { return c.redfishwrapper.Open(ctx) } // Close a connection to a BMC via redfish func (c *Conn) Close(ctx context.Context) error { return c.redfishwrapper.Close(ctx) } // Name returns the client provider name. func (c *Conn) Name() string { return ProviderName } // Compatible tests whether a BMC is compatible with the gofish provider func (c *Conn) Compatible(ctx context.Context) bool { err := c.Open(ctx) if err != nil { c.Log.V(2).WithValues( "provider", c.Name(), ).Info("warn", bmclibErrs.ErrCompatibilityCheck.Error(), err.Error()) return false } defer c.Close(ctx) if !c.redfishwrapper.VersionCompatible() { c.Log.V(2).WithValues( "provider", c.Name(), ).Info("info", bmclibErrs.ErrCompatibilityCheck.Error(), "incompatible redfish version") return false } _, err = c.PowerStateGet(ctx) if err != nil { c.Log.V(2).WithValues( "provider", c.Name(), ).Info("warn", bmclibErrs.ErrCompatibilityCheck.Error(), err.Error()) } return err == nil } // BmcReset power cycles the BMC func (c *Conn) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { return c.redfishwrapper.BMCReset(ctx, resetType) } // PowerStateGet gets the power state of a BMC machine func (c *Conn) PowerStateGet(ctx context.Context) (state string, err error) { return c.redfishwrapper.SystemPowerStatus(ctx) } // PowerSet sets the power state of a server func (c *Conn) PowerSet(ctx context.Context, state string) (ok bool, err error) { return c.redfishwrapper.PowerSet(ctx, state) } // BootDeviceSet sets the boot device func (c *Conn) BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { return c.redfishwrapper.SystemBootDeviceSet(ctx, bootDevice, setPersistent, efiBoot) } // BootDeviceOverrideGet gets the boot override device information func (c *Conn) BootDeviceOverrideGet(ctx context.Context) (bmc.BootDeviceOverride, error) { return c.redfishwrapper.GetBootDeviceOverride(ctx) } // SetVirtualMedia sets the virtual media func (c *Conn) SetVirtualMedia(ctx context.Context, kind string, mediaURL string) (ok bool, err error) { return c.redfishwrapper.SetVirtualMedia(ctx, kind, mediaURL) } // Inventory collects hardware inventory and install firmware information func (c *Conn) Inventory(ctx context.Context) (device *common.Device, err error) { return c.redfishwrapper.Inventory(ctx, c.failInventoryOnError) } // GetBiosConfiguration return bios configuration func (c *Conn) GetBiosConfiguration(ctx context.Context) (biosConfig map[string]string, err error) { return c.redfishwrapper.GetBiosConfiguration(ctx) } // SetBiosConfiguration set bios configuration func (c *Conn) SetBiosConfiguration(ctx context.Context, biosConfig map[string]string) (err error) { return c.redfishwrapper.SetBiosConfiguration(ctx, biosConfig) } // ResetBiosConfiguration set bios configuration func (c *Conn) ResetBiosConfiguration(ctx context.Context) (err error) { return c.redfishwrapper.ResetBiosConfiguration(ctx) } // SendNMI tells the BMC to issue an NMI to the device func (c *Conn) SendNMI(ctx context.Context) error { return c.redfishwrapper.SendNMI(ctx) } ================================================ FILE: providers/redfish/sel.go ================================================ package redfish import "context" func (c *Conn) ClearSystemEventLog(ctx context.Context) (err error) { return c.redfishwrapper.ClearSystemEventLog(ctx) } func (c *Conn) GetSystemEventLog(ctx context.Context) (entries [][]string, err error) { return c.redfishwrapper.GetSystemEventLog(ctx) } func (c *Conn) GetSystemEventLogRaw(ctx context.Context) (eventlog string, err error) { return c.redfishwrapper.GetSystemEventLogRaw(ctx) } ================================================ FILE: providers/redfish/sel_test.go ================================================ package redfish import ( "context" "testing" "github.com/stretchr/testify/assert" ) // Write tests for GetSystemEventLog func Test_GetSystemEventLog(t *testing.T) { entries, err := mockClient.GetSystemEventLog(context.TODO()) if err != nil { t.Fatal(err) } assert.NotNil(t, entries) assert.Equal(t, 2, len(entries)) } // Write tests for GetSystemEventLogRaw func Test_GetSystemEventLogRaw(t *testing.T) { eventlog, err := mockClient.GetSystemEventLogRaw(context.Background()) if err != nil { t.Fatal(err) } assert.NotNil(t, eventlog) } ================================================ FILE: providers/redfish/user.go ================================================ package redfish import ( "context" "github.com/bmc-toolbox/bmclib/v2/internal" "github.com/pkg/errors" "github.com/stmcginnis/gofish/schemas" ) var ( ErrNoUserSlotsAvailable = errors.New("no user account slots available") ErrUserNotPresent = errors.New("account with username was not found") ErrUserPassParams = errors.New("user and pass parameters required") ErrUserExists = errors.New("user exists") ErrInvalidUserRole = errors.New("invalid user role") ValidRoles = []string{"Administrator", "Operator", "ReadOnly", "None"} ) // UserRead returns a list of enabled user accounts func (c *Conn) UserRead(ctx context.Context) (users []map[string]string, err error) { service, err := c.redfishwrapper.AccountService() if err != nil { return nil, err } accounts, err := service.Accounts() if err != nil { return nil, err } users = make([]map[string]string, 0) for _, account := range accounts { if account.Enabled { user := map[string]string{ "ID": account.ID, "Name": account.Name, "Username": account.UserName, "RoleID": account.RoleID, } users = append(users, user) } } return users, nil } // UserUpdate updates a user password and role func (c *Conn) UserUpdate(ctx context.Context, user, pass, role string) (ok bool, err error) { service, err := c.redfishwrapper.AccountService() if err != nil { return false, err } accounts, err := service.Accounts() if err != nil { return false, err } for _, account := range accounts { if account.UserName == user { var change bool if pass != "" { account.Password = pass change = true } if role != "" { account.RoleID = role change = true } if change { err := account.Update() if err != nil { return false, err } return true, nil } } } return ok, ErrUserNotPresent } // UserCreate adds a new user account func (c *Conn) UserCreate(ctx context.Context, user, pass, role string) (ok bool, err error) { if !internal.StringInSlice(role, ValidRoles) { return false, ErrInvalidUserRole } if user == "" || pass == "" { return false, ErrUserPassParams } service, err := c.redfishwrapper.AccountService() if err != nil { return false, err } // fetch current list of accounts accounts, err := service.Accounts() if err != nil { return false, err } // identify account slot not in use for _, account := range accounts { // Dell iDracs don't want us to create accounts in these slots if internal.StringInSlice(account.ID, []string{"1"}) { continue } account := account if account.UserName == user { return false, errors.Wrap(ErrUserExists, user) } if !account.Enabled && account.UserName == "" { account.Enabled = true account.UserName = user account.Password = pass account.RoleID = role account.AccountTypes = []schemas.AccountTypes{"Redfish", "OEM"} err := account.Update() if err != nil { return false, err } return true, nil } } return false, ErrNoUserSlotsAvailable } // UserDelete deletes a user account func (c *Conn) UserDelete(ctx context.Context, user string) (ok bool, err error) { if user == "" { return false, ErrUserPassParams } service, err := c.redfishwrapper.AccountService() if err != nil { return false, err } // fetch current list of accounts accounts, err := service.Accounts() if err != nil { return false, err } // identify account slot for _, account := range accounts { // Dell iDracs don't want us to create/delete accounts in these slots if internal.StringInSlice(account.ID, []string{"1"}) { continue } account := account if account.UserName == user { account.Enabled = false account.UserName = "" account.Password = "" err := account.Update() if err != nil { return false, err } return true, nil } } return false, ErrUserNotPresent } ================================================ FILE: providers/rpc/doc.go ================================================ /* Package rpc is a provider that defines an HTTP request/response contract for handling BMC interactions. It allows users a simple way to interoperate with an existing/bespoke out-of-band management solution. The rpc provider request/response payloads are modeled after JSON-RPC 2.0, but are not JSON-RPC 2.0 compliant so as to allow for more flexibility and interoperability with existing systems. The rpc provider has options that can be set to include an HMAC signature in the request header. It follows the features found at https://webhooks.fyi/security/hmac, this includes hash algorithms sha256 and sha512, replay prevention, versioning, and key rotation. */ package rpc ================================================ FILE: providers/rpc/experimental.go ================================================ package rpc import ( "github.com/Jeffail/gabs/v2" "github.com/ghodss/yaml" ) // embedPayload will embed the RequestPayload into the given JSON object at the dot path notation location ("object.data"). func (p *RequestPayload) embedPayload(rawJSON []byte, dotPath string) ([]byte, error) { if rawJSON == nil { return rawJSON, nil } jdata2, err := yaml.YAMLToJSON(rawJSON) if err != nil { return nil, err } g, err := gabs.ParseJSON(jdata2) if err != nil { return nil, err } if _, err := g.SetP(p, dotPath); err != nil { return nil, err } return g.Bytes(), nil } ================================================ FILE: providers/rpc/http.go ================================================ package rpc import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strings" "time" ) // createRequest func (p *Provider) createRequest(ctx context.Context, rp RequestPayload) (*http.Request, error) { var data []byte if rj := p.Opts.Experimental.CustomRequestPayload; rj != nil && p.Opts.Experimental.DotPath != "" { d, err := rp.embedPayload(rj, p.Opts.Experimental.DotPath) if err != nil { return nil, err } data = d } else { d, err := json.Marshal(rp) if err != nil { return nil, err } data = d } req, err := http.NewRequestWithContext(ctx, p.Opts.Request.HTTPMethod, p.listenerURL.String(), bytes.NewReader(data)) if err != nil { return nil, err } for k, v := range p.Opts.Request.StaticHeaders { req.Header.Add(k, strings.Join(v, ",")) } if p.Opts.Request.HTTPContentType != "" { req.Header.Set("Content-Type", p.Opts.Request.HTTPContentType) } if p.Opts.Request.TimestampHeader != "" { req.Header.Add(p.Opts.Request.TimestampHeader, time.Now().Format(p.Opts.Request.TimestampFormat)) } return req, nil } func (p *Provider) handleResponse(statusCode int, headers http.Header, body *bytes.Buffer, reqKeysAndValues []any) (ResponsePayload, error) { kvs := reqKeysAndValues defer func() { if !p.LogNotificationsDisabled { kvs = append(kvs, responseKVS(statusCode, headers, body)...) p.Logger.Info("rpc notification details", kvs...) } }() res := ResponsePayload{} if err := json.Unmarshal(body.Bytes(), &res); err != nil { if statusCode != http.StatusOK { return ResponsePayload{}, fmt.Errorf("unexpected status code: %d, response error(optional): %v", statusCode, res.Error) } return ResponsePayload{}, fmt.Errorf("failed to parse response: %w", err) } if statusCode != http.StatusOK { return ResponsePayload{}, fmt.Errorf("unexpected status code: %d, response error(optional): %v", statusCode, res.Error) } return res, nil } ================================================ FILE: providers/rpc/http_test.go ================================================ package rpc import ( "bytes" "context" "encoding/json" "io" "net/http" "net/url" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) func testRequest(method, reqURL string, body RequestPayload, headers http.Header) *http.Request { buf := new(bytes.Buffer) _ = json.NewEncoder(buf).Encode(body) req, _ := http.NewRequestWithContext(context.Background(), method, reqURL, buf) req.Header = headers return req } func TestRequestKVS(t *testing.T) { tests := map[string]struct { req *http.Request expected []interface{} }{ "success": { req: testRequest( http.MethodPost, "http://example.com", RequestPayload{ID: 1, Host: "127.0.0.1", Method: "POST", Params: nil}, http.Header{"Content-Type": []string{"application/json"}}, ), expected: []interface{}{"request", requestDetails{ Body: RequestPayload{ ID: 1, Host: "127.0.0.1", Method: "POST", Params: nil, }, Headers: http.Header{"Content-Type": {"application/json"}}, URL: "http://example.com", Method: "POST", }}, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { buf := new(bytes.Buffer) _, _ = io.Copy(buf, tc.req.Body) kvs := requestKVS(tc.req.Method, tc.req.URL.String(), tc.req.Header, buf) if diff := cmp.Diff(kvs, tc.expected); diff != "" { t.Fatalf("requestKVS() mismatch (-want +got):\n%s", diff) } }) } } func TestResponseKVS(t *testing.T) { tests := map[string]struct { resp *http.Response expected []interface{} }{ "one": { resp: &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"id":1,"host":"127.0.0.1"}`))}, expected: []interface{}{"response", responseDetails{ StatusCode: 200, Headers: http.Header{"Content-Type": {"application/json"}}, Body: ResponsePayload{ID: 1, Host: "127.0.0.1"}, }}, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { buf := new(bytes.Buffer) _, _ = io.Copy(buf, tc.resp.Body) kvs := responseKVS(tc.resp.StatusCode, tc.resp.Header, buf) if diff := cmp.Diff(kvs, tc.expected); diff != "" { t.Fatalf("requestKVS() mismatch (-want +got):\n%s", diff) } }) } } func TestCreateRequest(t *testing.T) { tests := map[string]struct { cfg Provider body RequestPayload expected *http.Request }{ "success": { cfg: Provider{ Opts: Opts{ Request: RequestOpts{ HTTPMethod: http.MethodPost, HTTPContentType: "application/json", StaticHeaders: http.Header{"X-Test": []string{"test"}}, }, }, listenerURL: &url.URL{Scheme: "http", Host: "example.com"}, }, body: RequestPayload{ID: 1, Host: "127.0.0.1", Method: PowerSetMethod}, expected: &http.Request{ ContentLength: 52, Host: "example.com", Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Method: http.MethodPost, URL: &url.URL{Scheme: "http", Host: "example.com"}, Header: http.Header{"X-Test": {"test"}, "Content-Type": {"application/json"}}, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { data, _ := json.Marshal(tc.body) body := bytes.NewReader(data) tc.expected.Body = io.NopCloser(body) req, err := tc.cfg.createRequest(context.Background(), tc.body) if err != nil { t.Fatal(err) } if diff := cmp.Diff(req, tc.expected, cmpopts.IgnoreUnexported(http.Request{}, bytes.Reader{}), cmpopts.IgnoreFields(http.Request{}, "GetBody")); diff != "" { t.Fatalf("createRequest() mismatch (-got +want):\n%s", diff) } }) } } func TestContentSize(t *testing.T) { prov := New("http://127.0.0.1/rpc", "127.0.2.1", Secrets{SHA256: {"superSecret1"}}) _ = prov.Open(context.Background()) reqPayload := RequestPayload{ID: 1, Host: "127.0.0.1", Method: PowerGetMethod} req, err := prov.createRequest(context.Background(), reqPayload) if err != nil { t.Fatal(err) } if req.ContentLength > maxContentLenAllowed { t.Fatalf("unexpected content length: got: %d, want: %v", req.ContentLength, maxContentLenAllowed) } } ================================================ FILE: providers/rpc/logging.go ================================================ package rpc import ( "bytes" "encoding/json" "net/http" ) type requestDetails struct { Body RequestPayload `json:"body"` Headers http.Header `json:"headers"` URL string `json:"url"` Method string `json:"method"` } type responseDetails struct { StatusCode int `json:"statusCode"` Body ResponsePayload `json:"body"` Headers http.Header `json:"headers"` } // requestKVS returns a slice of key, value sets. Used for logging. func requestKVS(method, url string, headers http.Header, body *bytes.Buffer) []interface{} { var r requestDetails if body.Len() > 0 { var p RequestPayload _ = json.Unmarshal(body.Bytes(), &p) r = requestDetails{ Body: p, Headers: headers, URL: url, Method: method, } } return []interface{}{"request", r} } // responseKVS returns a slice of key, value sets. Used for logging. func responseKVS(statusCode int, headers http.Header, body *bytes.Buffer) []interface{} { var r responseDetails if body.Len() > 0 { var p ResponsePayload _ = json.Unmarshal(body.Bytes(), &p) r = responseDetails{ StatusCode: statusCode, Body: p, Headers: headers, } } return []interface{}{"response", r} } ================================================ FILE: providers/rpc/payload.go ================================================ package rpc import "fmt" type Method string const ( BootDeviceMethod Method = "setBootDevice" PowerSetMethod Method = "setPowerState" PowerGetMethod Method = "getPowerState" VirtualMediaMethod Method = "setVirtualMedia" PingMethod Method = "ping" ) // RequestPayload is the payload sent to the ConsumerURL. type RequestPayload struct { ID int64 `json:"id"` Host string `json:"host"` Method Method `json:"method"` Params any `json:"params,omitempty"` } // BootDeviceParams are the parameters options used when setting a boot device. type BootDeviceParams struct { Device string `json:"device"` Persistent bool `json:"persistent"` EFIBoot bool `json:"efiBoot"` } // PowerSetParams are the parameters options used when setting the power state. type PowerSetParams struct { State string `json:"state"` } // PowerGetParams are the parameters options used when getting the power state. type VirtualMediaParams struct { MediaURL string `json:"mediaUrl"` Kind string `json:"kind"` } // ResponsePayload is the payload received from the ConsumerURL. // The Result field is an interface{} so that different methods // can define the contract according to their needs. type ResponsePayload struct { // ID is the ID of the response. It should match the ID of the request but is not enforced. ID int64 `json:"id"` Host string `json:"host"` Result any `json:"result,omitempty"` Error *ResponseError `json:"error,omitempty"` } type ResponseError struct { Code int `json:"code"` Message string `json:"message"` } type PowerGetResult string const ( PoweredOn PowerGetResult = "on" PoweredOff PowerGetResult = "off" ) func (p PowerGetResult) String() string { return string(p) } func (r *ResponseError) String() string { return fmt.Sprintf("code: %v, message: %v", r.Code, r.Message) } ================================================ FILE: providers/rpc/rpc.go ================================================ package rpc import ( "bytes" "context" "fmt" "hash" "io" "net/http" "net/url" "reflect" "strings" "time" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" ) const ( // ProviderName for the RPC implementation. ProviderName = "rpc" // ProviderProtocol for the rpc implementation. ProviderProtocol = "http" // defaults timestampHeader = "X-BMCLIB-Timestamp" signatureHeader = "X-BMCLIB-Signature" contentType = "application/json" maxContentLenAllowed = 512 << (10 * 1) // 512KB // SHA256 is the SHA256 algorithm. SHA256 Algorithm = "sha256" // SHA256Short is the short version of the SHA256 algorithm. SHA256Short Algorithm = "256" // SHA512 is the SHA512 algorithm. SHA512 Algorithm = "sha512" // SHA512Short is the short version of the SHA512 algorithm. SHA512Short Algorithm = "512" ) // Features implemented by the RPC provider. var Features = registrar.Features{ providers.FeaturePowerSet, providers.FeaturePowerState, providers.FeatureBootDeviceSet, } // Algorithm is the type for HMAC algorithms. type Algorithm string // Secrets hold per algorithm slice secrets. // These secrets will be used to create HMAC signatures. type Secrets map[Algorithm][]string // Signatures hold per algorithm slice of signatures. type Signatures map[Algorithm][]string // Provider defines the configuration for sending rpc notifications. type Provider struct { // ConsumerURL is the URL where an rpc consumer/listener is running // and to which we will send and receive all notifications. ConsumerURL string // Host is the BMC ip address or hostname or identifier. Host string // HTTPClient is the http client used for all HTTP calls. HTTPClient *http.Client // Logger is the logger to use for logging. Logger logr.Logger // LogNotificationsDisabled determines whether responses from rpc consumer/listeners will be logged or not. LogNotificationsDisabled bool // Opts are the options for the rpc provider. Opts Opts // listenerURL is the URL of the rpc consumer/listener. listenerURL *url.URL } type Opts struct { // Request is the options used to create the rpc HTTP request. Request RequestOpts // Signature is the options used for adding an HMAC signature to an HTTP request. Signature SignatureOpts // HMAC is the options used to create a HMAC signature. HMAC HMACOpts // Experimental options. Experimental Experimental } type RequestOpts struct { // HTTPContentType is the content type to use for the rpc request notification. HTTPContentType string // HTTPMethod is the HTTP method to use for the rpc request notification. HTTPMethod string // StaticHeaders are predefined headers that will be added to every request. StaticHeaders http.Header // TimestampFormat is the time format for the timestamp header. TimestampFormat string // TimestampHeader is the header name that should contain the timestamp. Example: X-BMCLIB-Timestamp TimestampHeader string } type SignatureOpts struct { // HeaderName is the header name that should contain the signature(s). Example: X-BMCLIB-Signature HeaderName string // AppendAlgoToHeaderDisabled decides whether to append the algorithm to the signature header or not. // Example: X-BMCLIB-Signature becomes X-BMCLIB-Signature-256 // When set to true, a header will be added for each algorithm. Example: X-BMCLIB-Signature-256 and X-BMCLIB-Signature-512 AppendAlgoToHeaderDisabled bool // IncludedPayloadHeaders are headers whose values will be included in the signature payload. Example: given these headers in a request: // X-My-Header=123,X-Another=456, and IncludedPayloadHeaders := []string{"X-Another"}, the value of "X-Another" will be included in the signature payload. // All headers will be deduplicated. IncludedPayloadHeaders []string } type HMACOpts struct { // Hashes is a map of algorithms to a slice of hash.Hash (these are the hashed secrets). The slice is used to support multiple secrets. Hashes map[Algorithm][]hash.Hash // PrefixSigDisabled determines whether the algorithm will be prefixed to the signature. Example: sha256=abc123 PrefixSigDisabled bool // Secrets are a map of algorithms to secrets used for signing. Secrets Secrets } type Experimental struct { // CustomRequestPayload must be in json. CustomRequestPayload []byte // DotPath is the path to where the bmclib RequestPayload{} will be embedded. For example: object.data.body DotPath string } // New returns a new Config containing all the defaults for the rpc provider. func New(consumerURL string, host string, secrets Secrets) *Provider { // defaults c := &Provider{ Host: host, ConsumerURL: consumerURL, HTTPClient: httpclient.Build(), Logger: logr.Discard(), Opts: Opts{ Request: RequestOpts{ HTTPContentType: contentType, HTTPMethod: http.MethodPost, TimestampFormat: time.RFC3339, TimestampHeader: timestampHeader, }, Signature: SignatureOpts{ HeaderName: signatureHeader, IncludedPayloadHeaders: []string{}, }, HMAC: HMACOpts{ Hashes: map[Algorithm][]hash.Hash{}, Secrets: secrets, }, Experimental: Experimental{}, }, } if len(secrets) > 0 { c.Opts.HMAC.Hashes = CreateHashes(secrets) } return c } // Name returns the name of this rpc provider. // Implements bmc.Provider interface func (p *Provider) Name() string { return ProviderName } // Open a connection to the rpc consumer. // For the rpc provider, Open means validating the Config and // that communication with the rpc consumer can be established. func (p *Provider) Open(ctx context.Context) error { // 1. validate consumerURL is a properly formatted URL. // 2. validate that we can communicate with the rpc consumer. u, err := url.Parse(p.ConsumerURL) if err != nil { return err } p.listenerURL = u if _, err = p.process(ctx, RequestPayload{ ID: time.Now().UnixNano(), Host: p.Host, Method: PingMethod, }); err != nil { return err } return nil } // Close a connection to the rpc consumer. func (p *Provider) Close(_ context.Context) (err error) { return nil } // BootDeviceSet sends a next boot device rpc notification. func (p *Provider) BootDeviceSet(ctx context.Context, bootDevice string, setPersistent, efiBoot bool) (ok bool, err error) { rp := RequestPayload{ ID: time.Now().UnixNano(), Host: p.Host, Method: BootDeviceMethod, Params: BootDeviceParams{ Device: bootDevice, Persistent: setPersistent, EFIBoot: efiBoot, }, } resp, err := p.process(ctx, rp) if err != nil { return false, err } if resp.Error != nil && resp.Error.Code != 0 { return false, fmt.Errorf("error from rpc consumer: %v", resp.Error) } return true, nil } // PowerSet sets the power state of a BMC machine. func (p *Provider) PowerSet(ctx context.Context, state string) (ok bool, err error) { rp := RequestPayload{ ID: time.Now().UnixNano(), Host: p.Host, Method: PowerSetMethod, Params: PowerSetParams{ State: strings.ToLower(state), }, } resp, err := p.process(ctx, rp) if err != nil { return ok, err } if resp.Error != nil && resp.Error.Code != 0 { return ok, fmt.Errorf("error from rpc consumer: %v", resp.Error) } return true, nil } // PowerStateGet gets the power state of a BMC machine. func (p *Provider) PowerStateGet(ctx context.Context) (state string, err error) { rp := RequestPayload{ ID: time.Now().UnixNano(), Host: p.Host, Method: PowerGetMethod, } resp, err := p.process(ctx, rp) if err != nil { return "", err } if resp.Error != nil && resp.Error.Code != 0 { return "", fmt.Errorf("error from rpc consumer: %v", resp.Error) } s, ok := resp.Result.(string) if !ok { return "", fmt.Errorf("expected result equal to type string, got: %T", resp.Result) } return s, nil } // process is the main function for the roundtrip of rpc calls to the ConsumerURL. func (p *Provider) process(ctx context.Context, rp RequestPayload) (ResponsePayload, error) { // 1. create the HTTP request. // 2. create the signature payload. // 3. sign the signature payload. // 4. add signatures to the request as headers. // 5. request/response round trip. // 6. handle the response. req, err := p.createRequest(ctx, rp) if err != nil { return ResponsePayload{}, err } // create the signature payload reqBuf := new(bytes.Buffer) reqBody, err := req.GetBody() if err != nil { return ResponsePayload{}, fmt.Errorf("failed to get request body: %w", err) } if _, err := io.Copy(reqBuf, reqBody); err != nil { return ResponsePayload{}, fmt.Errorf("failed to read request body: %w", err) } headersForSig := http.Header{} for _, h := range p.Opts.Signature.IncludedPayloadHeaders { if val := req.Header.Get(h); val != "" { headersForSig.Add(h, val) } } sigPay := createSignaturePayload(reqBuf.Bytes(), headersForSig) // sign the signature payload sigs, err := sign(sigPay, p.Opts.HMAC.Hashes, p.Opts.HMAC.PrefixSigDisabled) if err != nil { return ResponsePayload{}, err } // add signatures to the request as headers. for algo, keys := range sigs { if len(sigs) > 0 { h := p.Opts.Signature.HeaderName if !p.Opts.Signature.AppendAlgoToHeaderDisabled { h = fmt.Sprintf("%s-%s", h, algo.ToShort()) } req.Header.Add(h, strings.Join(keys, ",")) } } // request/response round trip. kvs := requestKVS(req.Method, req.URL.String(), req.Header, reqBuf) kvs = append(kvs, []interface{}{"host", p.Host, "method", rp.Method, "consumerURL", p.ConsumerURL}...) if rp.Params != nil { kvs = append(kvs, []interface{}{"params", rp.Params}...) } resp, err := p.HTTPClient.Do(req) if err != nil { p.Logger.Error(err, "failed to send rpc notification", kvs...) return ResponsePayload{}, err } defer resp.Body.Close() // handle the response if resp.ContentLength > maxContentLenAllowed || resp.ContentLength < 0 { return ResponsePayload{}, fmt.Errorf("response body is too large: %d bytes, max allowed: %d bytes", resp.ContentLength, maxContentLenAllowed) } respBuf := new(bytes.Buffer) if _, err := io.CopyN(respBuf, resp.Body, resp.ContentLength); err != nil { return ResponsePayload{}, fmt.Errorf("failed to read response body: %w", err) } respPayload, err := p.handleResponse(resp.StatusCode, resp.Header, respBuf, kvs) if err != nil { return ResponsePayload{}, err } return respPayload, nil } // Transformer implements the mergo interfaces for merging custom types. func (p *Provider) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { switch typ { case reflect.TypeOf(logr.Logger{}): return func(dst, src reflect.Value) error { if dst.CanSet() { isZero := dst.MethodByName("GetSink") result := isZero.Call(nil) if result[0].IsNil() { dst.Set(src) } } return nil } } return nil } ================================================ FILE: providers/rpc/rpc_test.go ================================================ package rpc import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/go-cmp/cmp" ) func TestOpen(t *testing.T) { tests := map[string]struct { url string shouldErr bool }{ "success": {}, "bad url": {url: "%", shouldErr: true}, "failed request": {url: "127.1.1.1", shouldErr: true}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { svr := testConsumer{rp: ResponsePayload{}}.testServer() defer svr.Close() u := svr.URL if tc.url != "" { u = tc.url } c := New(u, "127.0.1.1", Secrets{SHA256: []string{"superSecret1"}}) ctx, cancel := context.WithCancel(context.Background()) defer cancel() if err := c.Open(ctx); err != nil && !tc.shouldErr { t.Fatal(err) } c.Close(ctx) }) } } func TestBootDeviceSet(t *testing.T) { tests := map[string]struct { url string shouldErr bool }{ "success": {}, "failure from consumer": {shouldErr: true}, "failed request": {url: "127.1.1.1", shouldErr: true}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { rsp := testConsumer{ rp: ResponsePayload{}, } if tc.shouldErr { rsp.rp.Error = &ResponseError{Code: 500, Message: "failed"} } svr := rsp.testServer() defer svr.Close() u := svr.URL if tc.url != "" { u = tc.url } c := New(u, "127.0.1.1", Secrets{SHA256: {"superSecret1"}}) ctx, cancel := context.WithCancel(context.Background()) defer cancel() _ = c.Open(ctx) if _, err := c.BootDeviceSet(ctx, "pxe", false, false); err != nil && !tc.shouldErr { t.Fatal(err) } else if err == nil && tc.shouldErr { t.Fatal("expected error, got none") } }) } } func TestPowerSet(t *testing.T) { tests := map[string]struct { url string powerState string shouldErr bool }{ "success": {powerState: "on"}, "failed request": {powerState: "on", url: "127.1.1.1", shouldErr: true}, "unknown state": {powerState: "unknown", shouldErr: true}, "failure from consumer": {powerState: "on", shouldErr: true}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { rsp := testConsumer{ rp: ResponsePayload{Result: tc.powerState}, } if tc.shouldErr { rsp.rp.Error = &ResponseError{Code: 500, Message: "failed"} } svr := rsp.testServer() defer svr.Close() u := svr.URL if tc.url != "" { u = tc.url } c := New(u, "127.0.1.1", Secrets{SHA256: {"superSecret1"}}) ctx, cancel := context.WithCancel(context.Background()) defer cancel() _ = c.Open(ctx) _, err := c.PowerSet(ctx, tc.powerState) if err != nil && !tc.shouldErr { t.Fatal(err) } }) } } func TestPowerStateGet(t *testing.T) { tests := map[string]struct { powerState string shouldErr bool url string }{ "success": {powerState: "on"}, "unknown state": {shouldErr: true}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { rsp := testConsumer{ rp: ResponsePayload{ID: 123, Host: "127.0.1.1", Result: tc.powerState}, } if tc.shouldErr { rsp.rp.Error = &ResponseError{Code: 500, Message: "failed"} } svr := rsp.testServer() defer svr.Close() u := svr.URL if tc.url != "" { u = tc.url } ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := New(u, "127.0.1.1", Secrets{SHA256: {"superSecret1"}}) _ = c.Open(ctx) gotState, err := c.PowerStateGet(ctx) if err != nil && !tc.shouldErr { t.Fatal(err) } if diff := cmp.Diff(gotState, tc.powerState); diff != "" { t.Fatal(diff) } }) } } func TestServerErrors(t *testing.T) { tests := map[string]struct { statusCode int shouldErr bool }{ "bad request": {statusCode: http.StatusBadRequest, shouldErr: true}, "not found": {statusCode: http.StatusNotFound, shouldErr: true}, "internal": {statusCode: http.StatusInternalServerError, shouldErr: true}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { rsp := testConsumer{ rp: ResponsePayload{Result: "on"}, statusCode: tc.statusCode, } svr := rsp.testServer() defer svr.Close() ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := New(svr.URL, "127.0.0.1", Secrets{SHA256: {"superSecret1"}}) if err := c.Open(ctx); err == nil { t.Fatal("expected error, got none") } }) } } type testConsumer struct { rp ResponsePayload statusCode int } func (t testConsumer) testServer() *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if t.statusCode != 0 { w.WriteHeader(t.statusCode) return } b, _ := json.Marshal(t.rp) _, _ = w.Write(b) })) } ================================================ FILE: providers/rpc/signature.go ================================================ package rpc import ( "crypto/hmac" "crypto/sha256" "crypto/sha512" "encoding/hex" "fmt" "hash" "net/http" "strings" ) type Hashes map[Algorithm][]hash.Hash // createSignaturePayload a signature payload is created by appending header values to the request body. // there is no delimiter between the body and the header values and all header values. func createSignaturePayload(body []byte, h http.Header) []byte { // add headers to signature payload, no space between values. for _, val := range h { body = append(body, []byte(strings.Join(val, ""))...) } return body } // sign signs the data with all the given hashes and returns the signatures. func sign(data []byte, h Hashes, prefixSigDisabled bool) (Signatures, error) { sigs := map[Algorithm][]string{} for algo, hshs := range h { for _, hsh := range hshs { if _, err := hsh.Write(data); err != nil { return nil, err } sig := hex.EncodeToString(hsh.Sum(nil)) if !prefixSigDisabled { sig = fmt.Sprintf("%s=%s", algo, sig) } sigs[algo] = append(sigs[algo], sig) // reset so Sign can be called multiple times. Otherwise, the next call will append to the previous one. hsh.Reset() } } return sigs, nil } // ToShort returns the short version of an algorithm. func (a Algorithm) ToShort() Algorithm { switch a { case SHA256: return SHA256Short case SHA512: return SHA512Short default: return a } } // NewSHA256 returns a map of SHA256 HMACs from the given secrets. func NewSHA256(secret ...string) Hashes { var hsh []hash.Hash for _, s := range secret { hsh = append(hsh, hmac.New(sha256.New, []byte(s))) } return Hashes{SHA256: hsh} } // NewSHA512 returns a map of SHA512 HMACs from the given secrets. func NewSHA512(secret ...string) Hashes { var hsh []hash.Hash for _, s := range secret { hsh = append(hsh, hmac.New(sha512.New, []byte(s))) } return Hashes{SHA512: hsh} } func mergeHashes(hs ...Hashes) Hashes { m := Hashes{} for _, h := range hs { for k, v := range h { m[k] = append(m[k], v...) } } return m } // CreateHashes creates a new hash for all secrets provided. func CreateHashes(s Secrets) map[Algorithm][]hash.Hash { h := map[Algorithm][]hash.Hash{} for algo, secrets := range s { switch algo { case SHA256, SHA256Short: h = mergeHashes(h, NewSHA256(secrets...)) case SHA512, SHA512Short: h = mergeHashes(h, NewSHA512(secrets...)) } } return h } ================================================ FILE: providers/supermicro/docs/x11.md ================================================ #### x11 XML API power commands power-off - immediate - `op=POWER_INFO.XML&r=(1,0)&_=` power-on - `op=POWER_INFO.XML&r=(1,1)&_=` power-off - `acpi/orderly - op=POWER_INFO.XML&r=(1,5)&_=` reset server - cold powercycle - `op=POWER_INFO.XML&r=(1,3)&_=` power cycle - `op=POWER_INFO.XML&r=(1,2)&_=` ref invocation ```go // powerCycle using SMC XML API func (c *x11) powerCycle(ctx context.Context) (bool, error) { payload := []byte(`op=POWER_INFO.XML&r=(1,3)&_=`) headers := map[string]string{ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } body, status, err := c.serviceClient.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { return false, err } if status != http.StatusOK { return false, unexpectedResponseErr(payload, body, status) } return true, nil } ``` ================================================ FILE: providers/supermicro/docs/x12.md ================================================ curl 'https://10.251.153.157/redfish/v1/UpdateService/upload' \ -H 'Accept: */*' \ -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' \ -H 'CSRF_TOKEN: p9lTd1+h0qsz/inooljtRbrja+1/z6nBRLuAKV6JJkM' \ -H 'Connection: keep-alive' \ -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytykIB3S8fDkno3cP' \ -H 'Cookie: SID=1rnGhJ9HoMI6JpP' \ -H 'Origin: https://10.251.153.157' \ -H 'Referer: https://10.251.153.157/cgi/url_redirect.cgi?url_name=topmenu' \ -H 'Sec-Fetch-Dest: empty' \ -H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Site: same-origin' \ -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' \ -H 'X-Auth-Token: o5jk3881vldnk3hdkn20wu5kg8brl18r' \ -H 'X-Requested-With: XMLHttpRequest' \ -H 'sec-ch-ua: "Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117"' \ -H 'sec-ch-ua-mobile: ?0' \ -H 'sec-ch-ua-platform: "macOS"' \ --data-raw $'------WebKitFormBoundarytykIB3S8fDkno3cP\r\nContent-Disposition: form-data; name="UpdateParameters"\r\n\r\n{"Targets":["/redfish/v1/Managers/1"],"@Redfish.OperationApplyTime":"OnStartUpdateRequest","Oem":{"Supermicro":{"BMC":{"PreserveCfg":true,"PreserveSdr":true,"PreserveSsl":true}}}}\r\n------WebKitFormBoundarytykIB3S8fDkno3cP\r\nContent-Disposition: form-data; name="UpdateFile"; filename="BMC_X12AST2600-F201MS_20220627_1.13.04_STDsp.bin"\r\nContent-Type: application/macbinary\r\n\r\n\r\n------WebKitFormBoundarytykIB3S8fDkno3cP--\r\n' \ --compressed // install parameters {"Targets":["/redfish/v1/Managers/1"],"@Redfish.OperationApplyTime":"OnStartUpdateRequest","Oem":{"Supermicro":{"BMC":{"PreserveCfg":true,"PreserveSdr":true,"PreserveSsl":true}}}} ## look for task with name "BMC Verify" ❯ curl 'https://10.251.153.157/redfish/v1/TaskService/Tasks/1' \ -H 'Accept: application/json, text/javascript, */*; q=0.01' \ -H 'CSRF_TOKEN: 10QMfkMegOzCe/WZZARLcs0cpxdDif8tSJcg5ZEnqVw' \ -H 'Connection: keep-alive' \ -H 'Content-Type: application/json' \ -H 'Cookie: SID=1rnGhJ9HoMI6JpP' \ -H 'X-Auth-Token: o5jk3881vldnk3hdkn20wu5kg8brl18r' \ --compressed \ --insecure {"@odata.type":"#Task.v1_4_3.Task","@odata.id":"/redfish/v1/TaskService/Tasks/1","Id":"1","Name":"BMC Verify","TaskState":"Completed","StartTime":"2023-10-06T07:53:25+00:00","EndTime":"2023-10-06T07:53:31+00:00","PercentComplete":100,"HidePayload":true,"TaskMonitor":"/redfish/v1/TaskMonitor/fa37JncCHryDsbzayy4cBWDxS22Jjzh","TaskStatus":"OK","Messages":[{"MessageId":"","RelatedProperties":[""],"Message":"","MessageArgs":[""],"Severity":""}],"Oem":{}} ## look for task with "BMC Update" { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/2", "Id": "2", "Name": "BMC Update", "TaskState": "Running", "StartTime": "2023-10-09T05:42:25+00:00", "PercentComplete": 2, "HidePayload": true, "TaskMonitor": "/redfish/v1/TaskMonitor/MaiRrV41mtzxlYvKWrO72tK0LK0e1zL", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [ "" ], "Message": "", "MessageArgs": [ "" ], "Severity": "" } ], "Oem": {} } curl 'https://10.251.153.157/cgi/upgrade_process.cgi' \ -H 'Accept: */*' \ -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' \ -H 'CSRF_TOKEN: +5/2t9ZcRuEzRg6MbTU2/j5Ils1VM2zf7uVImW/wVMI' \ -H 'Connection: keep-alive' \ -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \ -H 'Cookie: SID=qK4ZDz2cNet9nor' \ -H 'Origin: https://10.251.153.157' \ -H 'Referer: https://10.251.153.157/cgi/url_redirect.cgi?url_name=topmenu' \ -H 'Sec-Fetch-Dest: empty' \ -H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Site: same-origin' \ -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' \ -H 'X-Requested-With: XMLHttpRequest' \ -H 'sec-ch-ua: "Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117"' \ -H 'sec-ch-ua-mobile: ?0' \ -H 'sec-ch-ua-platform: "macOS"' \ --data-raw 'fwtype=255' \ --compressed \ --insecure { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/2", "Id": "2", "Name": "BMC Update", "TaskState": "Running", "StartTime": "2023-10-13T13:27:51+00:00", "PercentComplete": 5, "HidePayload": true, "TaskMonitor": "/redfish/v1/TaskMonitor/MaiRrV41mtzxlYvKWrO72tK0LK0e1zL", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [ "" ], "Message": "", "MessageArgs": [ "" ], "Severity": "" } ], "Oem": {} } ================================================ FILE: providers/supermicro/errors.go ================================================ package supermicro import ( "fmt" "strconv" "github.com/pkg/errors" ) var ( ErrQueryFRUInfo = errors.New("FRU information query returned error") ErrXMLAPIUnsupported = errors.New("XML API is unsupported") ErrModelUnknown = errors.New("Model number unknown") ErrModelUnsupported = errors.New("Model not supported") ErrBoardIDUnknown = errors.New("BoardID could not be identified") ErrUnexpectedResponse = errors.New("Unexpected response content") ErrUnexpectedStatusCode = errors.New("Unexpected status code") ) type UnexpectedResponseError struct { payload string response string statusCode string } func (e *UnexpectedResponseError) Error() string { return fmt.Sprintf( "unexpected response - statusCode: %s, payload: %s, response: %s", e.statusCode, e.payload, e.response, ) } func unexpectedResponseErr(payload, response []byte, statusCode int) error { return &UnexpectedResponseError{ string(payload), string(response), strconv.Itoa(statusCode), } } ================================================ FILE: providers/supermicro/firmware.go ================================================ package supermicro import ( "context" "os" "strings" "time" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" ) var ( // Its likely the X11 code works on all X11's // for now, we list only the ones its been tested on. // // board part numbers // supportedModels = []string{ "X11SSL-F", "X11SCM-F", "X11DPH-T", "X11SCH-F", "X11DGQ", "X11DPG-SN", "X11DPT-B", "X11SSE-F", "X12STH-SYS", "X12SPO-NTF", } errUploadTaskIDExpected = errors.New("expected an firmware upload taskID") ) // bmc client interface implementations methods func (c *Client) FirmwareInstallSteps(ctx context.Context, component string) ([]constants.FirmwareInstallStep, error) { if err := c.serviceClient.supportsFirmwareInstall(c.bmc.deviceModel()); err != nil { return nil, err } return c.bmc.firmwareInstallSteps(component) } func (c *Client) FirmwareUpload(ctx context.Context, component string, file *os.File) (taskID string, err error) { if err := c.serviceClient.supportsFirmwareInstall(c.bmc.deviceModel()); err != nil { return "", err } // expect atleast 5 minutes left in the deadline to proceed with the upload d, set := ctx.Deadline() if set && time.Until(d) < 5*time.Minute { return "", errors.New("remaining context deadline insufficient to perform update: " + time.Until(d).String()) } return c.bmc.firmwareUpload(ctx, component, file) } func (c *Client) FirmwareInstallUploaded(ctx context.Context, component, uploadTaskID string) (installTaskID string, err error) { if err := c.serviceClient.supportsFirmwareInstall(c.bmc.deviceModel()); err != nil { return "", err } // x11's don't return a upload Task ID, since the upload mechanism is not redfish if !strings.HasPrefix(strings.ToLower(c.bmc.deviceModel()), "x11") && uploadTaskID == "" { return "", errors.Wrap(errUploadTaskIDExpected, "device model: "+c.bmc.deviceModel()) } return c.bmc.firmwareInstallUploaded(ctx, component, uploadTaskID) } // FirmwareTaskStatus returns the status of a firmware related task queued on the BMC. func (c *Client) FirmwareTaskStatus(ctx context.Context, kind constants.FirmwareInstallStep, component, taskID, installVersion string) (state constants.TaskState, status string, err error) { if err := c.serviceClient.supportsFirmwareInstall(c.bmc.deviceModel()); err != nil { return "", "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, err.Error()) } component = strings.ToUpper(component) return c.bmc.firmwareTaskStatus(ctx, component, taskID) } ================================================ FILE: providers/supermicro/firmware_bios_test.go ================================================ package supermicro import ( "context" "io" "net/http" "net/http/httptest" "net/url" "testing" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" ) func Test_setComponentUpdateMisc(t *testing.T) { testcases := []struct { name string stage string errorContains string endpoint string handler func(http.ResponseWriter, *http.Request) }{ { "preUpdate", "preUpdate", "", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "application/x-www-form-urlencoded; charset=UTF-8", r.Header.Get("Content-Type")) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, `op=COMPONENT_UPDATE_MISC.XML&r=(0,0)&_=`, string(b)) _, _ = w.Write([]byte(` `)) }, }, { "postUpdate", "postUpdate", "", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "application/x-www-form-urlencoded; charset=UTF-8", r.Header.Get("Content-Type")) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, `op=COMPONENT_UPDATE_MISC.XML&r=(1,0)&_=`, string(b)) _, _ = w.Write([]byte(` `)) }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(tc.endpoint, tc.handler) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) assert.Nil(t, err) serviceClient.csrfToken = "foobar" client := &x11{serviceClient: serviceClient, log: logr.Discard()} if err := client.checkComponentUpdateMisc(context.Background(), tc.stage); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } assert.Nil(t, err) } }) } } func Test_setBIOSFirmwareInstallMode(t *testing.T) { testcases := []struct { name string errorContains string endpoint string handler func(http.ResponseWriter, *http.Request) }{ { "BIOS fw install lock acquired", "", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "application/x-www-form-urlencoded; charset=UTF-8", r.Header.Get("Content-Type")) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, `op=LOCK_UPLOAD_FW.XML&r=(0,0)&_=`, string(b)) _, _ = w.Write([]byte(` `)) }, }, { "lock not acquired", "BMC cold reset required", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "application/x-www-form-urlencoded; charset=UTF-8", r.Header.Get("Content-Type")) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, `op=LOCK_UPLOAD_FW.XML&r=(0,0)&_=`, string(b)) _, _ = w.Write([]byte(` `)) }, }, { "error returned", "400", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(400) }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(tc.endpoint, tc.handler) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) assert.Nil(t, err) serviceClient.csrfToken = "foobar" client := &x11{serviceClient: serviceClient, log: logr.Discard()} if err := client.setBMCFirmwareInstallMode(context.Background()); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } assert.Nil(t, err) } }) } } ================================================ FILE: providers/supermicro/fixtures/serviceroot.json ================================================ { "@odata.type": "#ServiceRoot.v1_5_2.ServiceRoot", "@odata.id": "/redfish/v1", "Id": "ServiceRoot", "Name": "Root Service", "RedfishVersion": "1.9.0", "UUID": "00000000-0000-0000-0000-3CECEFCEFEDA", "Systems": { "@odata.id": "/redfish/v1/Systems" }, "Chassis": { "@odata.id": "/redfish/v1/Chassis" }, "Managers": { "@odata.id": "/redfish/v1/Managers" }, "Tasks": { "@odata.id": "/redfish/v1/TaskService" }, "SessionService": { "@odata.id": "/redfish/v1/SessionService" }, "AccountService": { "@odata.id": "/redfish/v1/AccountService" }, "EventService": { "@odata.id": "/redfish/v1/EventService" }, "UpdateService": { "@odata.id": "/redfish/v1/UpdateService" }, "CertificateService": { "@odata.id": "/redfish/v1/CertificateService" }, "Registries": { "@odata.id": "/redfish/v1/Registries" }, "JsonSchemas": { "@odata.id": "/redfish/v1/JsonSchemas" }, "TelemetryService": { "@odata.id": "/redfish/v1/TelemetryService" }, "Links": { "Sessions": { "@odata.id": "/redfish/v1/SessionService/Sessions" } }, "ProtocolFeaturesSupported": { "FilterQuery": true, "SelectQuery": true, "ExcerptQuery": false, "OnlyMemberQuery": false, "ExpandQuery": { "Links": true, "NoLinks": true, "ExpandAll": true, "Levels": true, "MaxLevels": 2 } } } ================================================ FILE: providers/supermicro/floppy.go ================================================ package supermicro import ( "bytes" "context" "fmt" "io" "mime/multipart" "net/http" "net/textproto" "os" "path/filepath" "strings" "github.com/pkg/errors" ) var ( errFloppyImageMounted = errors.New("floppy image is currently mounted") ) func (c *Client) floppyImageMounted(ctx context.Context) (bool, error) { if err := c.serviceClient.redfishSession(ctx); err != nil { return false, err } inserted, err := c.serviceClient.redfish.InsertedVirtualMedia(ctx) if err != nil { return false, err } for _, media := range inserted { if strings.Contains(strings.ToLower(media), "floppy") { return true, nil } } return false, nil } func (c *Client) MountFloppyImage(ctx context.Context, image io.Reader) error { mounted, err := c.floppyImageMounted(ctx) if err != nil { return err } if mounted { return errFloppyImageMounted } var payloadBuffer bytes.Buffer type form struct { name string data io.Reader } formParts := []form{ { name: "img_file", data: image, }, } if c.serviceClient.csrfToken != "" { formParts = append(formParts, form{ name: "csrf-token", data: bytes.NewBufferString(c.serviceClient.csrfToken), }) } payloadWriter := multipart.NewWriter(&payloadBuffer) for _, part := range formParts { var partWriter io.Writer switch part.name { case "img_file": file, ok := part.data.(*os.File) if !ok { return errors.Wrap(ErrMultipartForm, "expected io.Reader for a floppy image file") } if partWriter, err = payloadWriter.CreateFormFile(part.name, filepath.Base(file.Name())); err != nil { return errors.Wrap(ErrMultipartForm, err.Error()) } case "csrf-token": // Add csrf token field h := make(textproto.MIMEHeader) // BMCs with newer firmware (>=1.74.09) accept the form with this name value // h.Set("Content-Disposition", `form-data; name="CSRF-TOKEN"`) // // the BMCs running older firmware (<=1.23.06) versions expects the name value in this format // and the newer firmware (>=1.74.09) seem to be backwards compatible with this name value format. h.Set("Content-Disposition", `form-data; name="CSRF_TOKEN"`) if partWriter, err = payloadWriter.CreatePart(h); err != nil { return errors.Wrap(ErrMultipartForm, err.Error()) } default: return errors.Wrap(ErrMultipartForm, "unexpected form part: "+part.name) } if _, err = io.Copy(partWriter, part.data); err != nil { return err } } payloadWriter.Close() resp, statusCode, err := c.serviceClient.query( ctx, "cgi/uimapin.cgi", http.MethodPost, bytes.NewReader(payloadBuffer.Bytes()), map[string]string{"Content-Type": payloadWriter.FormDataContentType()}, 0, ) if err != nil { return errors.Wrap(ErrMultipartForm, err.Error()) } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d %s", statusCode, resp) } return nil } func (c *Client) UnmountFloppyImage(ctx context.Context) error { mounted, err := c.floppyImageMounted(ctx) if err != nil { return err } if !mounted { return nil } resp, statusCode, err := c.serviceClient.query( ctx, "cgi/uimapout.cgi", http.MethodPost, nil, nil, 0, ) if err != nil { return err } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d %s", statusCode, resp) } return nil } ================================================ FILE: providers/supermicro/supermicro.go ================================================ package supermicro import ( "bytes" "context" "crypto/x509" "encoding/base64" "fmt" "io" "net/http" "net/http/httputil" "net/url" "os" "regexp" "strconv" "strings" "time" "github.com/bmc-toolbox/bmclib/v2/constants" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" "github.com/bmc-toolbox/bmclib/v2/internal/sum" "github.com/bmc-toolbox/bmclib/v2/providers" "github.com/bmc-toolbox/common" "github.com/stmcginnis/gofish/schemas" "github.com/go-logr/logr" "github.com/jacobweinstock/registrar" "github.com/pkg/errors" bmclibconsts "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" ) const ( // ProviderName for the provider Supermicro implementation ProviderName = "supermicro" // ProviderProtocol for the provider supermicro implementation ProviderProtocol = "vendorapi" ) var ( // Features implemented Features = registrar.Features{ providers.FeatureScreenshot, providers.FeatureMountFloppyImage, providers.FeatureUnmountFloppyImage, providers.FeatureFirmwareUpload, providers.FeatureFirmwareInstallUploaded, providers.FeatureFirmwareTaskStatus, providers.FeatureFirmwareInstallSteps, providers.FeatureInventoryRead, providers.FeaturePowerSet, providers.FeaturePowerState, providers.FeatureBmcReset, providers.FeatureGetBiosConfiguration, providers.FeatureSetBiosConfiguration, providers.FeatureSetBiosConfigurationFromFile, providers.FeatureResetBiosConfiguration, providers.FeatureBootProgress, } ) // supports // // product: SYS-5019C-MR, baseboard part number: X11SCM-F // - screen capture // - bios firmware install // - bmc firmware install // // product: SYS-510T-MR, baseboard part number: X12STH-SYS, X12SPO-NTF // - screen capture // - floppy image mount // product: 6029P-E1CR12L, baseboard part number: X11DPH-T // . - screen capture // - bios firmware install // - bmc firmware install // - floppy image mount type Config struct { HttpClient *http.Client Port string httpClientSetupFuncs []func(*http.Client) } // Option for setting optional Client values type Option func(*Config) func WithHttpClient(httpClient *http.Client) Option { return func(c *Config) { c.HttpClient = httpClient } } // WithSecureTLS returns an option that enables secure TLS with an optional cert pool. func WithSecureTLS(rootCAs *x509.CertPool) Option { return func(c *Config) { c.httpClientSetupFuncs = append(c.httpClientSetupFuncs, httpclient.SecureTLSOption(rootCAs)) } } func WithPort(port string) Option { return func(c *Config) { c.Port = port } } // Connection details type Client struct { serviceClient *serviceClient bmc bmcQueryor log logr.Logger } type bmcQueryor interface { firmwareInstallSteps(component string) ([]constants.FirmwareInstallStep, error) firmwareUpload(ctx context.Context, component string, file *os.File) (taskID string, err error) firmwareInstallUploaded(ctx context.Context, component, uploadTaskID string) (installTaskID string, err error) firmwareTaskStatus(ctx context.Context, component, taskID string) (state constants.TaskState, status string, err error) // query device model from the bmc queryDeviceModel(ctx context.Context) (model string, err error) // returns the device model, that was queried previously with queryDeviceModel deviceModel() (model string) supportsInstall(component string) error getBootProgress() (*schemas.BootProgress, error) bootComplete() (bool, error) } // New returns connection with a Supermicro client initialized func NewClient(host, user, pass string, log logr.Logger, opts ...Option) *Client { defaultConfig := &Config{ Port: "443", } for _, opt := range opts { opt(defaultConfig) } serviceClient := newBmcServiceClient( host, defaultConfig.Port, user, pass, httpclient.Build(defaultConfig.httpClientSetupFuncs...), ) return &Client{ serviceClient: serviceClient, log: log, } } func (c *Client) login(ctx context.Context, encodeCreds bool) error { var user, pass string if encodeCreds { user = base64.StdEncoding.EncodeToString([]byte(c.serviceClient.user)) pass = base64.StdEncoding.EncodeToString([]byte(c.serviceClient.pass)) } else { user = c.serviceClient.user pass = c.serviceClient.pass } data := fmt.Sprintf( "name=%s&pwd=%s&check=00", user, pass, ) headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} body, status, err := c.serviceClient.query(ctx, "cgi/login.cgi", http.MethodPost, bytes.NewBufferString(data), headers, 0) if err != nil { return err } if status != 200 { return errors.Wrap(ErrUnexpectedStatusCode, strconv.Itoa(status)) } // Older Supermicro boards return 200 for failed logins if !bytes.Contains(body, []byte(`url_redirect.cgi?url_name=mainmenu`)) && !bytes.Contains(body, []byte(`url_redirect.cgi?url_name=topmenu`)) { return ErrUnexpectedResponse } return nil } // Open a connection to a Supermicro BMC using the vendor API. func (c *Client) Open(ctx context.Context) (err error) { // called after a session was opened but further login dependencies failed closeWithError := func(ctx context.Context, err error) error { _ = c.Close(ctx) return err } // first attempt login with base64 encoded user,pass if err := c.login(ctx, true); err != nil { if !errors.Is(err, ErrUnexpectedResponse) && !errors.Is(err, ErrUnexpectedStatusCode) { return closeWithError(ctx, errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error())) } // retry with plain text user, pass if err2 := c.login(ctx, false); err2 != nil { return closeWithError(ctx, errors.Wrap(bmclibErrs.ErrLoginFailed, err2.Error())) } } contentsTopMenu, status, err := c.serviceClient.query(ctx, "cgi/url_redirect.cgi?url_name=topmenu", http.MethodGet, nil, nil, 0) if err != nil { return closeWithError(ctx, errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error())) } if status != 200 { return closeWithError(ctx, errors.Wrap(bmclibErrs.ErrLoginFailed, strconv.Itoa(status))) } // Note: older firmware version on the X11s don't use a CSRF token // so here theres no explicit requirement for it to be found. // // X11DPH-T 01.71.11 10/25/2019 csrfToken := parseToken(contentsTopMenu) c.serviceClient.setCsrfToken(csrfToken) c.bmc, err = c.bmcQueryor(ctx) if err != nil { return closeWithError(ctx, errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error())) } if err := c.serviceClient.redfishSession(ctx); err != nil { return closeWithError(ctx, errors.Wrap(bmclibErrs.ErrLoginFailed, err.Error())) } return nil } // PowerStateGet gets the power state of a BMC machine func (c *Client) PowerStateGet(ctx context.Context) (state string, err error) { if c.serviceClient == nil || c.serviceClient.redfish == nil { return "", errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized") } return c.serviceClient.redfish.SystemPowerStatus(ctx) } // PowerSet sets the power state of a server func (c *Client) PowerSet(ctx context.Context, state string) (ok bool, err error) { if c.serviceClient == nil || c.serviceClient.redfish == nil { return false, errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized") } return c.serviceClient.redfish.PowerSet(ctx, state) } // BmcReset power cycles the BMC func (c *Client) BmcReset(ctx context.Context, resetType string) (ok bool, err error) { if c.serviceClient == nil || c.serviceClient.redfish == nil { return false, errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized") } return c.serviceClient.redfish.BMCReset(ctx, resetType) } // Inventory collects hardware inventory and install firmware information func (c *Client) Inventory(ctx context.Context) (device *common.Device, err error) { if c.serviceClient == nil || c.serviceClient.redfish == nil { return nil, errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized") } return c.serviceClient.redfish.Inventory(ctx, false) } // GetBiosConfiguration return bios configuration func (c *Client) GetBiosConfiguration(ctx context.Context) (biosConfig map[string]string, err error) { if c.serviceClient == nil || c.serviceClient.sum == nil { return nil, errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized") } return c.serviceClient.sum.GetBiosConfiguration(ctx) } // SetBiosConfiguration set bios configuration func (c *Client) SetBiosConfiguration(ctx context.Context, biosConfig map[string]string) (err error) { if c.serviceClient == nil || c.serviceClient.sum == nil { return errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized") } return c.serviceClient.sum.SetBiosConfiguration(ctx, biosConfig) } // SetBiosConfigurationFromFile sets the bios configuration from a raw vendor config file func (c *Client) SetBiosConfigurationFromFile(ctx context.Context, cfg string) (err error) { if c.serviceClient == nil || c.serviceClient.sum == nil { return errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized") } return c.serviceClient.sum.SetBiosConfigurationFromFile(ctx, cfg) } // ResetBiosConfiguration sets the bios configuration back to "factory" defaults func (c *Client) ResetBiosConfiguration(ctx context.Context) (err error) { if c.serviceClient == nil || c.serviceClient.sum == nil { return errors.Wrap(bmclibErrs.ErrLoginFailed, "client not initialized") } return c.serviceClient.sum.ResetBiosConfiguration(ctx) } func (c *Client) bmcQueryor(ctx context.Context) (bmcQueryor, error) { x11 := newX11Client(c.serviceClient, c.log) x12 := newX12Client(c.serviceClient, c.log) var queryor bmcQueryor for _, bmc := range []bmcQueryor{x11, x12} { var err error // Note to maintainers: x12 lacks support for the ipmi.cgi endpoint, // which will lead to our graceful handling of ErrXMLAPIUnsupported below. _, err = bmc.queryDeviceModel(ctx) if err != nil { if errors.Is(err, ErrXMLAPIUnsupported) { continue } return nil, errors.Wrap(ErrModelUnknown, err.Error()) } queryor = bmc break } if queryor == nil { return nil, errors.Wrap(ErrModelUnknown, "failed to setup query client") } model := strings.ToLower(queryor.deviceModel()) if !strings.HasPrefix(model, "x12") && !strings.HasPrefix(model, "x11") { return nil, errors.Wrap(ErrModelUnsupported, "expected one of X11* or X12*, got:"+model) } return queryor, nil } func parseToken(body []byte) string { var key string if bytes.Contains(body, []byte(`CSRF-TOKEN`)) { key = "CSRF-TOKEN" } if bytes.Contains(body, []byte(`CSRF_TOKEN`)) { key = "CSRF_TOKEN" } if key == "" { return "" } re, err := regexp.Compile(fmt.Sprintf(`"%s", "(?P.*)"`, key)) if err != nil { return "" } found := re.FindSubmatch(body) if len(found) == 0 { return "" } return string(found[1]) } // Close a connection to a Supermicro BMC using the vendor API. func (c *Client) Close(ctx context.Context) error { if c.serviceClient.client == nil { return nil } _, status, err := c.serviceClient.query(ctx, "cgi/logout.cgi", http.MethodGet, nil, nil, 0) if err != nil { return errors.Wrap(bmclibErrs.ErrLogoutFailed, err.Error()) } if status != 200 { return errors.Wrap(bmclibErrs.ErrLogoutFailed, strconv.Itoa(status)) } if c.serviceClient.redfish != nil { err = c.serviceClient.redfish.Close(ctx) if err != nil { return errors.Wrap(bmclibErrs.ErrLogoutFailed, err.Error()) } c.serviceClient.redfish = nil } return nil } // Name returns the client provider name. func (c *Client) Name() string { return ProviderName } func (c *Client) Screenshot(ctx context.Context) (image []byte, fileType string, err error) { fileType = "jpg" // request screen preview to be saved if err := c.initScreenPreview(ctx); err != nil { return nil, "", err } // give the bmc a few seconds to store the screen preview time.Sleep(2 * time.Second) // retrieve screen preview image, errFetch := c.fetchScreenPreview(ctx) if errFetch != nil { return nil, "", err } return image, fileType, nil } func (c *Client) fetchScreenPreview(ctx context.Context) ([]byte, error) { headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} endpoint := "cgi/url_redirect.cgi?url_name=Snapshot&url_type=img" body, status, err := c.serviceClient.query(ctx, endpoint, http.MethodGet, nil, headers, 0) if err != nil { return nil, errors.Wrap(bmclibErrs.ErrScreenshot, strconv.Itoa(status)) } if status != 200 { return nil, errors.Wrap(bmclibErrs.ErrScreenshot, strconv.Itoa(status)) } return body, nil } func (c *Client) initScreenPreview(ctx context.Context) error { headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} data := "op=sys_preview&_=" body, status, err := c.serviceClient.query(ctx, "cgi/op.cgi", http.MethodPost, bytes.NewBufferString(data), headers, 0) if err != nil { return errors.Wrap(bmclibErrs.ErrScreenshot, err.Error()) } if status != 200 { return errors.Wrap(bmclibErrs.ErrScreenshot, strconv.Itoa(status)) } if !bytes.Contains(body, []byte(``)) { return errors.Wrap(bmclibErrs.ErrScreenshot, "unexpected response: "+string(body)) } return nil } type serviceClient struct { host string port string user string pass string csrfToken string client *http.Client redfish *redfishwrapper.Client sum *sum.Sum } func newBmcServiceClient(host, port, user, pass string, client *http.Client) *serviceClient { sc := &serviceClient{host: host, port: port, user: user, pass: pass, client: client} if !strings.HasPrefix(host, "https://") && !strings.HasPrefix(host, "http://") { sc.host = "https://" + host } // sum is only for firmware related operations. Failing the client entirely because of a sum error // means all the other functionality is not available. I don't think that is what we want. So, instead // of failing the function will just set sc.sum to nil. There are checks in place in this package to // handle sc.sum being nil. The tradeoff here is that the reason that sum failed, which from the current // code is only if the `sum` binary is not found, is not returned to the caller. So if firmware operations // are not working, binary not found is the reason. s, err := sum.New(host, user, pass) if err != nil { sc.sum = nil } else { sc.sum = s } return sc } func (c *serviceClient) setCsrfToken(t string) { c.csrfToken = t } func (c *serviceClient) redfishSession(ctx context.Context) (err error) { if c.redfish != nil && c.redfish.SessionActive() == nil { return nil } c.redfish = redfishwrapper.NewClient( c.host, c.port, c.user, c.pass, redfishwrapper.WithHTTPClient(c.client), ) if err := c.redfish.Open(ctx); err != nil { return err } return nil } func (c *serviceClient) supportsFirmwareInstall(model string) error { if model == "" { return errors.Wrap(ErrModelUnknown, "unable to determine firmware install compatibility") } for _, s := range supportedModels { if strings.EqualFold(s, model) { return nil } } return errors.Wrap(ErrModelUnsupported, "firmware install not supported for: "+model) } func (c *serviceClient) query(ctx context.Context, endpoint, method string, payload io.Reader, headers map[string]string, contentLength int64) ([]byte, int, error) { var body []byte var err error var req *http.Request host := c.host if c.port != "" { host = c.host + ":" + c.port } hostEndpoint := fmt.Sprintf("%s/%s", host, endpoint) req, err = http.NewRequestWithContext(ctx, method, hostEndpoint, payload) if err != nil { return nil, 0, err } if c.csrfToken != "" { req.Header.Add("Csrf-Token", c.csrfToken) // because old firmware req.Header.Add("CSRF_TOKEN", c.csrfToken) } // required on X11SCM-F with 1.23.06 and older BMC firmware // https://go.googlesource.com/go/+/go1.20/src/net/http/request.go#124 req.Host, err = hostIP(c.host) if err != nil { return nil, 0, err } // required on X11SCM-F with 1.23.06 and older BMC firmware req.Header.Add("Referer", c.host) for k, v := range headers { req.Header.Add(k, v) } // Content-Length headers are ignored, unless defined in this manner // https://go.googlesource.com/go/+/go1.20/src/net/http/request.go#165 // https://go.googlesource.com/go/+/go1.20/src/net/http/request.go#91 if contentLength > 0 { req.ContentLength = contentLength } endpointURL, err := url.Parse(hostEndpoint) if err != nil { return nil, 0, err } // include session cookie for _, cookie := range c.client.Jar.Cookies(endpointURL) { if cookie.Name == "SID" && cookie.Value != "" { req.AddCookie(cookie) } } var reqDump []byte if os.Getenv(bmclibconsts.EnvEnableDebug) == "true" { reqDump, _ = httputil.DumpRequestOut(req, true) } resp, err := c.client.Do(req) if err != nil { return body, 0, err } // cookies are visible after the request has been made, so we dump the request and cookies here // https://github.com/golang/go/issues/22745 if os.Getenv(bmclibconsts.EnvEnableDebug) == "true" { fmt.Println(string(reqDump)) for _, v := range req.Cookies() { header := "Cookie: " + v.String() + "\r" fmt.Println(header) } } // debug dump response if os.Getenv(bmclibconsts.EnvEnableDebug) == "true" { respDump, _ := httputil.DumpResponse(resp, true) fmt.Println(string(respDump)) } body, err = io.ReadAll(resp.Body) if err != nil { return body, 0, err } defer resp.Body.Close() return body, resp.StatusCode, nil } func hostIP(hostURL string) (string, error) { hostURLParsed, err := url.Parse(hostURL) if err != nil { return "", err } if strings.Contains(hostURLParsed.Host, ":") { return strings.Split(hostURLParsed.Host, ":")[0], nil } return hostURLParsed.Host, nil } // SendNMI tells the BMC to issue an NMI to the device func (c *Client) SendNMI(ctx context.Context) error { return c.serviceClient.redfish.SendNMI(ctx) } // GetBootProgress allows a caller to follow along as the system goes through its boot sequence func (c *Client) GetBootProgress() (*schemas.BootProgress, error) { return c.bmc.getBootProgress() } // BootComplete checks if this system has reached the last state for boot func (c *Client) BootComplete() (bool, error) { return c.bmc.bootComplete() } ================================================ FILE: providers/supermicro/supermicro_test.go ================================================ package supermicro import ( "context" "io" "log" "net/http" "net/http/httptest" "net/url" "os" "testing" "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" ) const ( fixturesDir = "./fixtures" ) func TestParseToken(t *testing.T) { testcases := []struct { name string body []byte expectToken string }{ { "token with key type 1 found", []byte(``), "A0v9gild518yF36XZ6jqNZNsOUrHiEpkvM+QHKKVTFw", }, { "token with key type 2 found", []byte(` `), "Te6xHPx3NDhDmL4T21cZ/tXWbzatZQ3JHbQUCF5Hkns", }, { "token with key type 3 found", []byte(`
`), "fYQ/Xhd1AvA+kP/bM/tO5mhOzv4eM5evCOH/YSuBN70", }, { "token with key type 4 found", []byte(``), "RYjdEjWIhU+PCRFMBP2ZRPPePcQ4n3dM3s+rCgTnBBU", }, { "token with key type 5 found", []byte(``), "RYjdEjWIhU+PCRFMBP2ZRPPePcQ4n3dM3s+rCgTnBBU", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { gotToken := parseToken(tc.body) assert.Equal(t, tc.expectToken, gotToken) }) } } func mustReadFile(t *testing.T, filename string) []byte { t.Helper() fixture := fixturesDir + "/" + filename fh, err := os.Open(fixture) if err != nil { log.Fatal(err) } defer fh.Close() b, err := io.ReadAll(fh) if err != nil { log.Fatal(err) } return b } var endpointFunc = func(t *testing.T, file string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // expect either GET or Delete methods if r.Method != http.MethodGet && r.Method != http.MethodPost && r.Method != http.MethodDelete { w.WriteHeader(http.StatusNotFound) return } _, _ = w.Write(mustReadFile(t, file)) } } func TestOpen(t *testing.T) { type handlerFuncMap map[string]func(http.ResponseWriter, *http.Request) testcases := []struct { name string errorContains string user string pass string handlerFuncMap handlerFuncMap }{ { "happy path", "", "foo", "bar", handlerFuncMap{ "/": func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }, "/redfish/v1/": endpointFunc(t, "serviceroot.json"), // first request to login "/cgi/login.cgi": func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, http.MethodPost) assert.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, `name=Zm9v&pwd=YmFy&check=00`, string(b)) response := []byte(` `) _, _ = w.Write(response) }, // second request for the csrf token "/cgi/url_redirect.cgi": func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, http.MethodGet) assert.Equal(t, "url_name=topmenu", r.URL.RawQuery) _, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } response := []byte(``) _, _ = w.Write(response) }, // request for model "/cgi/ipmi.cgi": func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, http.MethodPost) assert.Equal(t, "application/x-www-form-urlencoded; charset=UTF-8", r.Header.Get("Content-Type")) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, `op=FRU_INFO.XML&r=(0,0)&_=`, string(b)) _, _ = w.Write([]byte(` `)) }, }, }, { "login error", "failed to login", "foo", "bar", handlerFuncMap{ "/cgi/login.cgi": func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, http.MethodPost) assert.Equal(t, r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") response := []byte(`barf`) w.WriteHeader(401) _, _ = w.Write(response) }, }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() for endpoint, handler := range tc.handlerFuncMap { mux.HandleFunc(endpoint, handler) } server := httptest.NewTLSServer(mux) defer server.Close() server.Config.ErrorLog = log.New(os.Stdout, "foo", 3) parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } client := NewClient(parsedURL.Hostname(), tc.user, tc.pass, logr.Discard(), WithPort(parsedURL.Port())) client.serviceClient.redfish = redfishwrapper.NewClient( parsedURL.Hostname(), parsedURL.Port(), tc.user, tc.pass, redfishwrapper.WithHTTPClient(client.serviceClient.client), ) err = client.Open(context.Background()) if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } assert.Nil(t, err) }) } } func TestClose(t *testing.T) { testcases := []struct { name string errorContains string user string pass string endpoint string handler func(http.ResponseWriter, *http.Request) }{ { "happy path", "", "foo", "bar", "/cgi/logout.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, http.MethodGet) _, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } w.WriteHeader(200) }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(tc.endpoint, tc.handler) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } client := NewClient(parsedURL.Hostname(), tc.user, tc.pass, logr.Discard(), WithPort(parsedURL.Port())) err = client.Close(context.Background()) if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } assert.Nil(t, err) assert.Nil(t, client.serviceClient.redfish) }) } } func TestInitScreenPreview(t *testing.T) { testcases := []struct { name string errorContains string endpoint string handler func(http.ResponseWriter, *http.Request) }{ { "happy path", "", "/cgi/op.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, http.MethodPost) assert.Equal(t, r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, `op=sys_preview&_=`, string(b)) _, _ = w.Write([]byte(` `)) }, }, { "error returned", "400", "/cgi/op.cgi", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(400) }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(tc.endpoint, tc.handler) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) err = client.initScreenPreview(context.Background()) if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } assert.Nil(t, err) }) } } func TestFetchScreenPreview(t *testing.T) { testcases := []struct { name string expectImage []byte errorContains string endpoint string handler func(http.ResponseWriter, *http.Request) }{ { "happy path", []byte(`fake image is fake`), "", "/cgi/url_redirect.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, http.MethodGet) assert.Equal(t, r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") assert.Equal(t, "url_name=Snapshot&url_type=img", r.URL.RawQuery) _, _ = w.Write([]byte(`fake image is fake`)) }, }, { "error returned", nil, "400", "/cgi/url_redirect.cgi", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(400) }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(tc.endpoint, tc.handler) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } client := NewClient(parsedURL.Hostname(), "foo", "bar", logr.Discard(), WithPort(parsedURL.Port())) image, err := client.fetchScreenPreview(context.Background()) if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } assert.Nil(t, err) assert.Equal(t, tc.expectImage, image) }) } } ================================================ FILE: providers/supermicro/types.go ================================================ package supermicro type IPMI struct { FruInfo *FruInfo `xml:"FRU_INFO,omitempty"` } // FruInfo contains the FRU information type FruInfo struct { Board *Board `xml:"BOARD,omitempty"` } // Board contains the product baseboard information type Board struct { MfcName string `xml:"MFC_NAME,attr"` PartNum string `xml:"PART_NUM,attr"` ProdName string `xml:"PROD_NAME,attr"` SerialNum string `xml:"SERIAL_NUM,attr"` } ================================================ FILE: providers/supermicro/x11.go ================================================ package supermicro import ( "bytes" "context" "encoding/xml" "fmt" "net/http" "os" "slices" "strings" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/bmc-toolbox/common" "github.com/go-logr/logr" "github.com/pkg/errors" "github.com/stmcginnis/gofish/oem/smc" "github.com/stmcginnis/gofish/schemas" ) type x11 struct { *serviceClient model string log logr.Logger } func newX11Client(client *serviceClient, logger logr.Logger) bmcQueryor { return &x11{ serviceClient: client, log: logger, } } func (c *x11) deviceModel() string { return c.model } func (c *x11) queryDeviceModel(ctx context.Context) (string, error) { model, err := c.deviceModelFromFruInfo(ctx) if err != nil { // Identify BoardID from Redfish since fru info failed to return the information model, err2 := c.deviceModelFromBoardID(ctx) if err2 != nil { return "", errors.Wrap(err, err2.Error()) } c.model = model return model, nil } c.model = model return model, nil } func (c *x11) deviceModelFromFruInfo(ctx context.Context) (string, error) { errBoardPartNumUnknown := errors.New("baseboard part number unknown") data, err := c.fruInfo(ctx) if err != nil { if strings.Contains(err.Error(), "404") { return "", ErrXMLAPIUnsupported } return "", err } partNum := strings.TrimSpace(data.Board.PartNum) if data.Board == nil || partNum == "" { return "", errors.Wrap(errBoardPartNumUnknown, "baseboard part number empty") } return common.FormatProductName(partNum), nil } func (c *x11) deviceModelFromBoardID(ctx context.Context) (string, error) { if err := c.redfishSession(ctx); err != nil { return "", err } chassis, err := c.redfish.Chassis(ctx) if err != nil { return "", err } var boardID string for _, ch := range chassis { smcChassis, err := smc.FromChassis(ch) if err != nil { return "", errors.Wrap(ErrBoardIDUnknown, err.Error()) } if smcChassis.BoardID != "" { boardID = smcChassis.BoardID break } } if boardID == "" { return "", ErrBoardIDUnknown } model := common.SupermicroModelFromBoardID(boardID) if model == "" { return "", errors.Wrap(ErrModelUnknown, "unable to identify model from board ID: "+boardID) } return model, nil } func (c *x11) fruInfo(ctx context.Context) (*FruInfo, error) { headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} payload := "op=FRU_INFO.XML&r=(0,0)&_=" body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBufferString(payload), headers, 0) if err != nil { return nil, errors.Wrap(ErrQueryFRUInfo, err.Error()) } if status != 200 { return nil, unexpectedResponseErr([]byte(payload), body, status) } if !bytes.Contains(body, []byte(``)) { return nil, unexpectedResponseErr([]byte(payload), body, status) } data := &IPMI{} if err := xml.Unmarshal(body, data); err != nil { return nil, errors.Wrap(ErrQueryFRUInfo, err.Error()) } return data.FruInfo, nil } func (c *x11) supportsInstall(component string) error { errComponentNotSupported := fmt.Errorf("component %s on device %s not supported", component, c.model) supported := []string{common.SlugBIOS, common.SlugBMC} if !slices.Contains(supported, strings.ToUpper(component)) { return errComponentNotSupported } return nil } func (c *x11) firmwareInstallSteps(component string) ([]constants.FirmwareInstallStep, error) { if err := c.supportsInstall(component); err != nil { return nil, err } steps := []constants.FirmwareInstallStep{ constants.FirmwareInstallStepUpload, constants.FirmwareInstallStepInstallUploaded, constants.FirmwareInstallStepInstallStatus, } // On a failure the X11 BMC has to be removed from the // flash mode - which is done through a BMC reset if strings.EqualFold(component, common.SlugBMC) { steps = append(steps, constants.FirmwareInstallStepResetBMCOnInstallFailure) } return steps, nil } func (c *x11) firmwareUpload(ctx context.Context, component string, file *os.File) (string, error) { component = strings.ToUpper(component) switch component { case common.SlugBIOS: return "", c.firmwareUploadBIOS(ctx, file) case common.SlugBMC: return "", c.firmwareUploadBMC(ctx, file) } return "", errors.Wrap(bmclibErrs.ErrFirmwareInstall, "component unsupported: "+component) } func (c *x11) firmwareInstallUploaded(ctx context.Context, component, _ string) (string, error) { component = strings.ToUpper(component) switch component { case common.SlugBIOS: return "", c.firmwareInstallUploadedBIOS(ctx) case common.SlugBMC: return "", c.initiateBMCFirmwareInstall(ctx) } return "", errors.Wrap(bmclibErrs.ErrFirmwareInstallUploaded, "component unsupported: "+component) } func (c *x11) firmwareTaskStatus(ctx context.Context, component, _ string) (state constants.TaskState, status string, err error) { component = strings.ToUpper(component) switch component { case common.SlugBIOS: return c.statusBIOSFirmwareInstall(ctx) case common.SlugBMC: return c.statusBMCFirmwareInstall(ctx) } return "", "", errors.Wrap(bmclibErrs.ErrFirmwareTaskStatus, "component unsupported: "+component) } func (c *x11) getBootProgress() (*schemas.BootProgress, error) { return nil, fmt.Errorf("%w: not supported on x11 models", bmclibErrs.ErrRedfishVersionIncompatible) } func (c *x11) bootComplete() (bool, error) { return false, fmt.Errorf("%w: not supported on x11 models", bmclibErrs.ErrRedfishVersionIncompatible) } ================================================ FILE: providers/supermicro/x11_firmware_bios.go ================================================ package supermicro import ( "bytes" "context" "fmt" "io" "mime/multipart" "net/http" "net/textproto" "os" "path/filepath" "strconv" "strings" "time" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" "github.com/pkg/errors" ) func (c *x11) firmwareUploadBIOS(ctx context.Context, reader io.Reader) error { var err error c.log.V(2).Info("set firmware install mode", "ip", c.host, "component", "BIOS", "model", c.model) // 0. pre flash mode requisite if err := c.checkComponentUpdateMisc(ctx, "preUpdate"); err != nil { return err } // 1. set the device to flash mode - prepares the flash err = c.setBIOSFirmwareInstallMode(ctx) if err != nil { return errors.Wrap(err, ErrFirmwareInstallMode.Error()) } err = c.setBiosUpdateStart(ctx) if err != nil { return err } c.log.V(2).Info("uploading firmware", "ip", c.host, "component", "BIOS", "model", c.model) // 2. upload firmware image file err = c.uploadBIOSFirmware(ctx, reader) if err != nil { return err } c.log.V(2).Info("verifying uploaded firmware", "ip", c.host, "component", "BIOS", "model", c.model) // 3. BMC verifies the uploaded firmware version return c.verifyBIOSFirmwareVersion(ctx) } func (c *x11) firmwareInstallUploadedBIOS(ctx context.Context) error { c.log.V(2).Info("initiating firmware install", "ip", c.host, "component", "BIOS", "model", c.model) // pre install requisite err := c.setBIOSOp(ctx) if err != nil { return err } // 4. Run the firmware install process return c.initiateBIOSFirmwareInstall(ctx) } // checks component update status func (c *x11) checkComponentUpdateMisc(ctx context.Context, stage string) error { var payload, expectResponse []byte switch stage { case "preUpdate": payload = []byte(`op=COMPONENT_UPDATE_MISC.XML&r=(0,0)&_=`) // RES=-1 indicates the BMC is not in BIOS update mode expectResponse = []byte(``) case "postUpdate": payload = []byte(`op=COMPONENT_UPDATE_MISC.XML&r=(1,0)&_=`) // RES=0 indicates the BMC is in BIOS update mode expectResponse = []byte(``) // When SYSOFF=1 the system requires a power cycle default: return errors.New("unknown stage: " + stage) } headers := map[string]string{ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { return err } if status != http.StatusOK || !bytes.Contains(body, expectResponse) { // this indicates the BMC is in firmware update mode and now requires a reset // calling BIOS_UNLOCK.xml doesn't help here if stage == "preUpdate" && bytes.Contains(body, []byte(``)) { return bmclibErrs.ErrBMCColdResetRequired } if bytes.Contains(body, []byte(``)) { return bmclibErrs.ErrHostPowercycleRequired } if stage == "postUpdate" && bytes.Contains(body, []byte(``)) { return bmclibErrs.ErrSessionExpired } return unexpectedResponseErr(payload, body, status) } return nil } func (c *x11) setBIOSFirmwareInstallMode(ctx context.Context) error { payload := []byte(`op=BIOS_UPLOAD.XML&r=(0,0)&_=`) headers := map[string]string{ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { return err } if status != http.StatusOK { return unexpectedResponseErr(payload, body, status) } switch { case bytes.Contains(body, []byte(`LOCK_FW_UPLOAD RES="0"`)): // This response indicates another web session that initiated the firmware upload has the lock, // the BMC cannot be reset through a web session, nor can any other user obtain the firmware upload lock. // Since the firmware upload lock is associated with the cookie that initiated the request only the initiating session can cancel it. // // The only way to get out of this situation is through an IPMI (or redfish?) based BMC cold reset. /// // The caller must check if a firmware update is in progress before proceeding with the reset. // // If after multiple calls to check the install progress - the progress seems stalled at 1% // it indicates no update was active, and the BMC can be reset. // // 1 return errors.Wrap( bmclibErrs.ErrBMCColdResetRequired, "firmware upload mode active, another session may have initiated an install", ) case bytes.Contains(body, []byte(`LOCK_FW_UPLOAD RES="1"`)): return nil default: return unexpectedResponseErr(payload, body, status) } } func (c *x11) setBiosUpdateStart(ctx context.Context) error { payload := []byte(`op=BIOS_UPDATE_START.XML&r=(1,0)&_=`) headers := map[string]string{ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { return err } // yep, the endpoint returns 500 even when successful if status != http.StatusOK && status != 500 { return unexpectedResponseErr(payload, body, status) } return nil } // ------WebKitFormBoundaryXIAavwG4xzohdB6k // Content-Disposition: form-data; name="bios_rom"; filename="BIOS_X11SCM-1B0F_20220916_1.9_STDsp.bin" // Content-Type: application/macbinary // // ------WebKitFormBoundaryXIAavwG4xzohdB6k // Content-Disposition: form-data; name="CSRF-TOKEN" // // OO8+cjamaZZOMf6ZiGDY3Lw+7O20r5lR8aI8ByuTo3E // ------WebKitFormBoundaryXIAavwG4xzohdB6k-- func (c *x11) uploadBIOSFirmware(ctx context.Context, fwReader io.Reader) error { var payloadBuffer bytes.Buffer var err error type form struct { name string data io.Reader } formParts := []form{ { name: "bios_rom", data: fwReader, }, } if c.csrfToken != "" { formParts = append(formParts, form{ name: "csrf-token", data: bytes.NewBufferString(c.csrfToken), }) } payloadWriter := multipart.NewWriter(&payloadBuffer) for _, part := range formParts { var partWriter io.Writer switch part.name { case "bios_rom": file, ok := part.data.(*os.File) if !ok { return errors.Wrap(ErrMultipartForm, "expected io.Reader on firmware image file") } if partWriter, err = payloadWriter.CreateFormFile(part.name, filepath.Base(file.Name())); err != nil { return errors.Wrap(ErrMultipartForm, err.Error()) } case "csrf-token": // Add csrf token field h := make(textproto.MIMEHeader) // BMCs with newer firmware (>=1.74.09) accept the form with this name value // h.Set("Content-Disposition", `form-data; name="CSRF-TOKEN"`) // // the BMCs running older firmware (<=1.23.06) versions expects the name value in this format // and the newer firmware (>=1.74.09) seem to be backwards compatible with this name value format. h.Set("Content-Disposition", `form-data; name="CSRF_TOKEN"`) if partWriter, err = payloadWriter.CreatePart(h); err != nil { return errors.Wrap(ErrMultipartForm, err.Error()) } default: return errors.Wrap(ErrMultipartForm, "unexpected form part: "+part.name) } if _, err = io.Copy(partWriter, part.data); err != nil { return err } } payloadWriter.Close() resp, statusCode, err := c.query( ctx, "cgi/bios_upload.cgi", http.MethodPost, bytes.NewReader(payloadBuffer.Bytes()), map[string]string{"Content-Type": payloadWriter.FormDataContentType()}, 0, ) if err != nil { return errors.Wrap(ErrMultipartForm, err.Error()) } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d %s", statusCode, resp) } return nil } func (c *x11) verifyBIOSFirmwareVersion(ctx context.Context) error { payload := []byte(`op=BIOS_UPDATE_CHECK.XML&r=(0,0)&_=`) expectResponse := []byte(``) headers := map[string]string{ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { return err } if status != http.StatusOK || !bytes.Contains(body, expectResponse) { return unexpectedResponseErr(payload, body, status) } payload = []byte(`op=BIOS_REV.XML&_=`) expectResponse = []byte(``) headers := map[string]string{ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { return err } if status != http.StatusOK || !bytes.Contains(body, expectResponse) { return unexpectedResponseErr(payload, body, status) } return nil } func (c *x11) initiateBIOSFirmwareInstall(ctx context.Context) error { // save all current SMBIOS, NVRAM, ME configuration payload := []byte(`op=main_biosupdate&_=`) expectResponse := []byte(`ok`) headers := map[string]string{ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } // don't spend much time on this call since it doesn't return and holds the connection. sctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() body, status, err := c.query(sctx, "cgi/op.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { // this endpoint generally times out - its expected if strings.Contains(err.Error(), "context deadline exceeded") || strings.Contains(err.Error(), "operation timed out") { return nil } return err } if status != http.StatusOK || !bytes.Contains(body, expectResponse) { return unexpectedResponseErr(payload, body, status) } return nil } func (c *x11) setBIOSUpdateDone(ctx context.Context) error { payload := []byte(`op=BIOS_UPDATE_DONE.XML&r=(1,0)&_=`) headers := map[string]string{ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { return err } // yep, the endpoint returns 500 even when successful if status != http.StatusOK && status != 500 { return unexpectedResponseErr(payload, body, status) } return nil } // statusBIOSFirmwareInstall returns the status of the firmware install process func (c *x11) statusBIOSFirmwareInstall(ctx context.Context) (state constants.TaskState, status string, err error) { payload := []byte(`fwtype=1&_`) headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} resp, httpStatus, err := c.query(ctx, "cgi/upgrade_process.cgi", http.MethodPost, bytes.NewReader(payload), headers, 0) if err != nil { return "", "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, err.Error()) } if httpStatus != http.StatusOK { return "", "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "Unexpected http status code: "+strconv.Itoa(httpStatus)) } // if theres html or no xml in the response, the session expired // at the end of the install the BMC resets itself and the response is in HTML. if bytes.Contains(resp, []byte(``)) || !bytes.Contains(resp, []byte(``)) { // reopen session here, check firmware install status return constants.Unknown, "session expired/unexpected response", bmclibErrs.ErrSessionExpired } // as long as the response is xml, the firmware install is running part := strings.Split(string(resp), "")[1] percent := strings.Split(part, "")[0] percent += "%" switch { // 1% indicates the file has been uploaded and the firmware install is not yet initiated case bytes.Contains(resp, []byte("0")) && bytes.Contains(resp, []byte("1")): return constants.Failed, percent, bmclibErrs.ErrBMCColdResetRequired // 0% along with the check on the component endpoint indicates theres no update in progress case (bytes.Contains(resp, []byte("0")) && bytes.Contains(resp, []byte("0"))): if err := c.checkComponentUpdateMisc(ctx, "postUpdate"); err != nil { if errors.Is(err, bmclibErrs.ErrHostPowercycleRequired) { return constants.PowerCycleHost, percent, nil } } return constants.Complete, "all done!", nil // status 0 and 100% indicates the update is complete and requires a few post update calls case bytes.Contains(resp, []byte("0")) && bytes.Contains(resp, []byte("100")): // TODO: create a new bmc method FirmwarePostInstall() // notifies the BMC the BIOS update is done if err := c.setBIOSUpdateDone(ctx); err != nil { return "", "", err } // tells the BMC it can get out of the BIOS update mode if err := c.checkComponentUpdateMisc(ctx, "postUpdate"); err != nil { if errors.Is(err, bmclibErrs.ErrHostPowercycleRequired) { return constants.PowerCycleHost, percent, nil } return constants.PowerCycleHost, percent, err } return constants.PowerCycleHost, percent, nil // status 8 and percent 0 indicates its initializing the update case bytes.Contains(resp, []byte("8")) && bytes.Contains(resp, []byte("0")): return constants.Running, percent, nil // status 8 and any other percent value indicates its running case bytes.Contains(resp, []byte("8")) && bytes.Contains(resp, []byte("")): return constants.Running, percent, nil } return constants.Unknown, "", nil } ================================================ FILE: providers/supermicro/x11_firmware_bmc.go ================================================ package supermicro import ( "bytes" "context" "fmt" "io" "mime/multipart" "net/http" "net/textproto" "os" "path/filepath" "strconv" "strings" "time" "github.com/pkg/errors" "github.com/bmc-toolbox/bmclib/v2/constants" bmclibErrs "github.com/bmc-toolbox/bmclib/v2/errors" ) var ( ErrFirmwareInstallMode = errors.New("firmware install mode error") ErrMultipartForm = errors.New("multipart form error") ) func (c *x11) firmwareUploadBMC(ctx context.Context, reader io.Reader) error { c.log.V(2).Info("setting device to firmware install mode", "ip", c.host, "component", "BMC", "model", c.model) // 1. set the device to flash mode - prepares the flash err := c.setBMCFirmwareInstallMode(ctx) if err != nil { return err } c.log.V(2).Info("uploading firmware", "ip", c.host, "component", "BMC", "model", c.model) // 2. upload firmware image file err = c.uploadBMCFirmware(ctx, reader) if err != nil { return err } c.log.V(2).Info("verifying uploaded firmware", "ip", c.host, "component", "BMC", "model", c.model) // 3. BMC verifies the uploaded firmware version return c.verifyBMCFirmwareVersion(ctx) } func (c *x11) setBMCFirmwareInstallMode(ctx context.Context) error { payload := []byte(`op=LOCK_UPLOAD_FW.XML&r=(0,0)&_=`) headers := map[string]string{ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { return errors.Wrap(ErrFirmwareInstallMode, err.Error()) } if status != http.StatusOK { return errors.Wrap(ErrFirmwareInstallMode, "Unexpected status code: "+strconv.Itoa(status)) } switch { case bytes.Contains(body, []byte(`LOCK_FW_UPLOAD RES="0"`)): // This response indicates another web session that initiated the firmware upload has the lock, // the BMC cannot be reset through a web session, nor can any other user obtain the firmware upload lock. // Since the firmware upload lock is associated with the cookie that initiated the request only the initiating session can cancel it. // // The only way to get out of this situation is through an IPMI (or redfish?) based BMC cold reset. /// // The caller must check if a firmware update is in progress before proceeding with the reset. // // If after multiple calls to check the install progress - the progress seems stalled at 1% // it indicates no update was active, and the BMC can be reset. // // 1 return errors.Wrap( bmclibErrs.ErrBMCColdResetRequired, "unable to acquire lock for firmware upload, check if an update is in progress", ) case bytes.Contains(body, []byte(`LOCK_FW_UPLOAD RES="1"`)): return nil default: return errors.Wrap(ErrFirmwareInstallMode, "set firmware install mode returned unexpected response body") } } // -----------------------------212212001131894333502018521064 // Content-Disposition: form-data; name="fw_image"; filename="BMC_X11AST2500-4101MS_20221020_01.74.09_STDsp.bin" // Content-Type: application/macbinary // // ... contents... // // -----------------------------348113760313214626342869148824 // Content-Disposition: form-data; name="CSRF-TOKEN" // // JhVe1BUiWzOVQdvXUKn7ClsQ5xffq8StMOxG7ZNlpKs // -----------------------------348113760313214626342869148824-- func (c *x11) uploadBMCFirmware(ctx context.Context, fwReader io.Reader) error { var payloadBuffer bytes.Buffer var err error type form struct { name string data io.Reader } formParts := []form{ { name: "fw_image", data: fwReader, }, } if c.csrfToken != "" { formParts = append(formParts, form{ name: "csrf-token", data: bytes.NewBufferString(c.csrfToken), }) } payloadWriter := multipart.NewWriter(&payloadBuffer) for _, part := range formParts { var partWriter io.Writer switch part.name { case "fw_image": file, ok := part.data.(*os.File) if !ok { return errors.Wrap(ErrMultipartForm, "expected io.Reader on firmware image file") } if partWriter, err = payloadWriter.CreateFormFile(part.name, filepath.Base(file.Name())); err != nil { return errors.Wrap(ErrMultipartForm, err.Error()) } case "csrf-token": // Add csrf token field h := make(textproto.MIMEHeader) // BMCs with newer firmware (>=1.74.09) accept the form with this name value // h.Set("Content-Disposition", `form-data; name="CSRF-TOKEN"`) // // the BMCs running older firmware (<=1.23.06) versions expects the name value in this format // this seems to be backwards compatible with the newer firmware. h.Set("Content-Disposition", `form-data; name="CSRF_TOKEN"`) if partWriter, err = payloadWriter.CreatePart(h); err != nil { return errors.Wrap(ErrMultipartForm, err.Error()) } default: return errors.Wrap(ErrMultipartForm, "unexpected form part: "+part.name) } if _, err = io.Copy(partWriter, part.data); err != nil { return err } } payloadWriter.Close() resp, statusCode, err := c.query( ctx, "cgi/oem_firmware_upload.cgi", http.MethodPost, bytes.NewReader(payloadBuffer.Bytes()), map[string]string{"Content-Type": payloadWriter.FormDataContentType()}, 0, ) if err != nil { return errors.Wrap(ErrMultipartForm, err.Error()) } if statusCode != http.StatusOK { return fmt.Errorf("non 200 response: %d %s", statusCode, resp) } return nil } func (c *x11) verifyBMCFirmwareVersion(ctx context.Context) error { errUnexpectedResponse := errors.New("unexpected response") payload := []byte(`op=UPLOAD_FW_VERSION.XML&r=(0,0)&_=`) headers := map[string]string{ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", } body, status, err := c.query(ctx, "cgi/ipmi.cgi", http.MethodPost, bytes.NewBuffer(payload), headers, 0) if err != nil { return err } if status != 200 { return errors.Wrap(ErrFirmwareInstallMode, "Unexpected status code: "+strconv.Itoa(status)) } if !bytes.Contains(body, []byte(`FW_VERSION NEW`)) { return errors.Wrap(errUnexpectedResponse, string(body)) } return nil } // initiate BMC firmware install process func (c *x11) initiateBMCFirmwareInstall(ctx context.Context) error { // preserve all configuration, sensor data and SSL certs(?) during upgrade payload := "op=main_fwupdate&preserve_config=1&preserve_sdr=1&preserve_ssl=1" headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} // don't spend much time on this call since it doesn't return and holds the connection. sctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() _, status, err := c.query(sctx, "cgi/op.cgi", http.MethodPost, bytes.NewBufferString(payload), headers, 0) if err != nil { // this operation causes the BMC to go AWOL and not send any response // so we ignore the error here, the caller can invoke FirmwareInstallStatus in the same session, // to check the install status to determine install progress. // whats returned is a *url.Error{} and errors.Is(err, context.DeadlineExceeded) doesn't seem to match // so a string contains it is. if strings.Contains(err.Error(), "context deadline exceeded") || strings.Contains(err.Error(), "operation timed out") { return nil } return err } if status != 200 { return errors.Wrap(ErrFirmwareInstallMode, "Unexpected status code: "+strconv.Itoa(status)) } return nil } // statusBMCFirmwareInstall returns the status of the firmware install process func (c *x11) statusBMCFirmwareInstall(ctx context.Context) (state constants.TaskState, status string, err error) { payload := []byte(`fwtype=0&_`) headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} resp, httpStatus, err := c.query(ctx, "cgi/upgrade_process.cgi", http.MethodPost, bytes.NewReader(payload), headers, 0) if err != nil { return constants.Unknown, "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, err.Error()) } if httpStatus != http.StatusOK { return constants.Unknown, "", errors.Wrap(bmclibErrs.ErrFirmwareInstallStatus, "Unexpected http status code: "+strconv.Itoa(httpStatus)) } // if theres html or no xml in the response, the session expired // at the end of the install the BMC resets itself and the response is in HTML. if bytes.Contains(resp, []byte(``)) || !bytes.Contains(resp, []byte(``)) { // reopen session here, check firmware install status return constants.Unknown, "session expired/unexpected response", bmclibErrs.ErrSessionExpired } // as long as the response is xml, the firmware install is running part := strings.Split(string(resp), "")[1] percent := strings.Split(part, "")[0] percent += "%" switch percent { // TODO: // X11DPH-T - returns percent 0 all the time // // 0% indicates its either not running or complete case "0%", "100%": return constants.Complete, percent, nil // until 2% its initializing case "1%", "2%": return constants.Initializing, percent, nil // any other percent value indicates its active default: return constants.Running, percent, nil } } ================================================ FILE: providers/supermicro/x11_firmware_bmc_test.go ================================================ package supermicro import ( "bytes" "context" "io" "mime" "mime/multipart" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "testing" "github.com/bmc-toolbox/bmclib/v2/constants" "github.com/bmc-toolbox/bmclib/v2/internal/httpclient" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" ) func TestX11SetBMCFirmwareInstallMode(t *testing.T) { testcases := []struct { name string errorContains string endpoint string handler func(http.ResponseWriter, *http.Request) }{ { "BMC fw install lock acquired", "", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "application/x-www-form-urlencoded; charset=UTF-8", r.Header.Get("Content-Type")) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, `op=LOCK_UPLOAD_FW.XML&r=(0,0)&_=`, string(b)) _, _ = w.Write([]byte(` `)) }, }, { "lock not acquired", "BMC cold reset required", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "application/x-www-form-urlencoded; charset=UTF-8", r.Header.Get("Content-Type")) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, `op=LOCK_UPLOAD_FW.XML&r=(0,0)&_=`, string(b)) _, _ = w.Write([]byte(` `)) }, }, { "error returned", "400", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(400) }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(tc.endpoint, tc.handler) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) assert.Nil(t, err) client := &x11{serviceClient: serviceClient, log: logr.Discard()} if err := client.setBMCFirmwareInstallMode(context.Background()); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } assert.Nil(t, err) } }) } } func TestX11UploadBMCFirmware(t *testing.T) { testcases := []struct { name string errorContains string endpoint string fwFilename string fwFileContents string handler func(http.ResponseWriter, *http.Request) }{ { "upload works", "", "/cgi/oem_firmware_upload.cgi", "blob.bin", "dummy fw image", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } // validate content type mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) assert.Nil(t, err) assert.Equal(t, "multipart/form-data", mediaType) // read form parts from boundary reader := multipart.NewReader(bytes.NewReader(b), params["boundary"]) // validate firmware image part part, err := reader.NextPart() assert.Nil(t, err) assert.Equal(t, `form-data; name="fw_image"; filename="blob.bin"`, part.Header.Get("Content-Disposition")) // validate csrf-token part part, err = reader.NextPart() assert.Nil(t, err) assert.Equal(t, `form-data; name="CSRF_TOKEN"`, part.Header.Get("Content-Disposition")) }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(tc.endpoint, tc.handler) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } // create tmp firmware file var fwReader *os.File if tc.fwFilename != "" { tmpdir := t.TempDir() binPath := filepath.Join(tmpdir, tc.fwFilename) err := os.WriteFile(binPath, []byte(tc.fwFileContents), 0600) if err != nil { t.Fatal(err) } fwReader, err = os.Open(binPath) if err != nil { t.Fatalf("%s -> %s", err.Error(), binPath) } defer os.Remove(binPath) } serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) assert.Nil(t, err) serviceClient.csrfToken = "foobar" client := &x11{serviceClient: serviceClient, log: logr.Discard()} if err := client.uploadBMCFirmware(context.Background(), fwReader); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } assert.Nil(t, err) } }) } } func TestX11VerifyBMCFirmwareVersion(t *testing.T) { testcases := []struct { name string errorContains string endpoint string handler func(http.ResponseWriter, *http.Request) }{ { "verify successful", "", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, []byte(`op=UPLOAD_FW_VERSION.XML&r=(0,0)&_=`), b) resp := []byte(` `) _, err = w.Write(resp) if err != nil { t.Fatal(err) } }, }, { "unexpected response", "unexpected response", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) resp := []byte(`bad bmc does not comply`) _, err := w.Write(resp) if err != nil { t.Fatal(err) } }, }, { "unexpected status code", "Unexpected status code: 403", "/cgi/ipmi.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) w.WriteHeader(http.StatusForbidden) }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(tc.endpoint, tc.handler) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) assert.Nil(t, err) serviceClient.csrfToken = "foobar" client := &x11{serviceClient: serviceClient, log: logr.Discard()} if err := client.verifyBMCFirmwareVersion(context.Background()); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } assert.Nil(t, err) } }) } } func TestX11InitiateBMCFirmwareInstall(t *testing.T) { testcases := []struct { name string errorContains string endpoint string handler func(http.ResponseWriter, *http.Request) }{ { "install intiated successfully", "", "/cgi/op.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, []byte(`op=main_fwupdate&preserve_config=1&preserve_sdr=1&preserve_ssl=1`), b) resp := []byte(`Upgrade progress.. 1%`) _, err = w.Write(resp) if err != nil { t.Fatal(err) } }, }, { "unexpected response", "unexpected response", "/cgi/op.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) resp := []byte(`bad bmc does not comply`) _, err := w.Write(resp) if err != nil { t.Fatal(err) } }, }, { "unexpected status code", "Unexpected status code: 403", "/cgi/op.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) w.WriteHeader(http.StatusForbidden) }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(tc.endpoint, tc.handler) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) assert.Nil(t, err) serviceClient.csrfToken = "foobar" client := &x11{serviceClient: serviceClient, log: logr.Discard()} if err := client.initiateBMCFirmwareInstall(context.Background()); err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } assert.Nil(t, err) } }) } } func TestX11StatusBMCFirmwareInstall(t *testing.T) { testcases := []struct { name string expectState constants.TaskState expectStatus string errorContains string endpoint string handler func(http.ResponseWriter, *http.Request) }{ { "state complete 0", constants.Complete, "0%", "", "/cgi/upgrade_process.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, []byte(`fwtype=0&_`), b) resp := []byte(` 0 `) _, err = w.Write(resp) if err != nil { t.Fatal(err) } }, }, { "state complete 100", constants.Complete, "100%", "", "/cgi/upgrade_process.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, []byte(`fwtype=0&_`), b) resp := []byte(` 100 `) _, err = w.Write(resp) if err != nil { t.Fatal(err) } }, }, { "state initializing", constants.Initializing, "1%", "", "/cgi/upgrade_process.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, []byte(`fwtype=0&_`), b) resp := []byte(` 1 `) _, err = w.Write(resp) if err != nil { t.Fatal(err) } }, }, { "status running", constants.Running, "95%", "", "/cgi/upgrade_process.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, []byte(`fwtype=0&_`), b) resp := []byte(` 95 `) _, err = w.Write(resp) if err != nil { t.Fatal(err) } }, }, { "status unknown", constants.Unknown, "", "session expired", "/cgi/upgrade_process.cgi", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) b, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } assert.Equal(t, []byte(`fwtype=0&_`), b) resp := []byte(` uh what `) _, err = w.Write(resp) if err != nil { t.Fatal(err) } }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc(tc.endpoint, tc.handler) server := httptest.NewTLSServer(mux) defer server.Close() parsedURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } serviceClient := newBmcServiceClient(parsedURL.Hostname(), parsedURL.Port(), "foo", "bar", httpclient.Build()) assert.Nil(t, err) serviceClient.csrfToken = "foobar" client := &x11{serviceClient: serviceClient, log: logr.Discard()} gotState, gotStatus, err := client.statusBMCFirmwareInstall(context.Background()) if err != nil { if tc.errorContains != "" { assert.ErrorContains(t, err, tc.errorContains) return } } assert.Nil(t, err) assert.Equal(t, tc.expectState, gotState) assert.Equal(t, tc.expectStatus, gotStatus) }) } } ================================================ FILE: providers/supermicro/x12.go ================================================ package supermicro import ( "context" "encoding/json" "fmt" "os" "slices" "strings" "github.com/bmc-toolbox/bmclib/v2/constants" brrs "github.com/bmc-toolbox/bmclib/v2/errors" rfw "github.com/bmc-toolbox/bmclib/v2/internal/redfishwrapper" "github.com/bmc-toolbox/common" "github.com/go-logr/logr" "github.com/pkg/errors" "github.com/stmcginnis/gofish/schemas" ) type x12 struct { *serviceClient model string log logr.Logger } func newX12Client(client *serviceClient, logger logr.Logger) bmcQueryor { return &x12{ serviceClient: client, log: logger, } } func (c *x12) deviceModel() string { return c.model } func (c *x12) queryDeviceModel(ctx context.Context) (string, error) { if err := c.redfishSession(ctx); err != nil { return "", err } _, model, err := c.redfish.DeviceVendorModel(ctx) if err != nil { return "", err } if model == "" { return "", errors.Wrap(ErrModelUnknown, "empty value") } c.model = common.FormatProductName(model) return c.model, nil } var ( errUploadTaskIDEmpty = errors.New("firmware upload request returned empty firmware upload verify TaskID") ) func (c *x12) supportsInstall(component string) error { errComponentNotSupported := fmt.Errorf("component %s on device %s not supported", component, c.model) supported := []string{common.SlugBIOS, common.SlugBMC} if !slices.Contains(supported, strings.ToUpper(component)) { return errComponentNotSupported } return nil } func (c *x12) firmwareInstallSteps(component string) ([]constants.FirmwareInstallStep, error) { if err := c.supportsInstall(component); err != nil { return nil, err } return []constants.FirmwareInstallStep{ constants.FirmwareInstallStepUpload, constants.FirmwareInstallStepUploadStatus, constants.FirmwareInstallStepInstallUploaded, constants.FirmwareInstallStepInstallStatus, }, nil } // upload firmware func (c *x12) firmwareUpload(ctx context.Context, component string, file *os.File) (taskID string, err error) { if err = c.supportsInstall(component); err != nil { return "", err } err = c.firmwareTaskActive(ctx, component) if err != nil { return "", err } targetID, err := c.redfishOdataID(ctx, component) if err != nil { return "", err } params, err := c.redfishParameters(component, targetID) if err != nil { return "", err } taskID, err = c.redfish.FirmwareUpload(ctx, file, params) if err != nil { if strings.Contains(err.Error(), "OemFirmwareAlreadyInUpdateMode") { return "", errors.Wrap(brrs.ErrBMCColdResetRequired, "BMC currently in update mode, either continue the update OR if no update is currently running - reset the BMC") } return "", errors.Wrap(err, "error in firmware upload") } if taskID == "" { return "", errUploadTaskIDEmpty } return taskID, nil } // returns an error when a bmc firmware install is active func (c *x12) firmwareTaskActive(ctx context.Context, component string) error { tasks, err := c.redfish.Tasks(ctx) if err != nil { return errors.Wrap(err, "error querying redfish tasks") } for _, t := range tasks { t := t if stateFinalized(t.TaskState) { continue } if err := noTasksRunning(component, t); err != nil { return err } } return nil } // noTasksRunning returns an error if a firmware related task was found active func noTasksRunning(component string, t *schemas.Task) error { errTaskActive := errors.New("A firmware task was found active for component: " + component) const ( // The redfish task name when the BMC is verifies the uploaded BMC firmware. verifyBMCFirmware = "BMC Verify" // The redfish task name when the BMC is installing the uploaded BMC firmware. updateBMCFirmware = "BMC Update" // The redfish task name when the BMC is verifies the uploaded BIOS firmware. verifyBIOSFirmware = "BIOS Verify" // The redfish task name when the BMC is installing the uploaded BIOS firmware. updateBIOSFirmware = "BIOS Update" ) var verifyTaskName, updateTaskName string switch strings.ToUpper(component) { case common.SlugBMC: verifyTaskName = verifyBMCFirmware updateTaskName = updateBMCFirmware case common.SlugBIOS: verifyTaskName = verifyBIOSFirmware updateTaskName = updateBIOSFirmware } taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", t.ID, t.TaskState, t.TaskStatus) switch t.Name { case verifyTaskName: return errors.Wrap(errTaskActive, taskInfo) case updateTaskName: return errors.Wrap(errTaskActive, taskInfo) default: return nil } } func stateFinalized(s schemas.TaskState) bool { finalized := []schemas.TaskState{ schemas.CompletedTaskState, schemas.CancelledTaskState, schemas.InterruptedTaskState, schemas.ExceptionTaskState, } return slices.Contains(finalized, s) } type Supermicro struct { BIOS map[string]bool `json:"BIOS,omitempty"` BMC map[string]bool `json:"BMC,omitempty"` } type OEM struct { Supermicro `json:"Supermicro"` } // redfish OEM fw install parameters func (c *x12) biosFwInstallParams() (map[string]bool, error) { switch c.model { case "x12spo-ntf": return map[string]bool{ "PreserveME": false, "PreserveNVRAM": false, "PreserveSMBIOS": true, "BackupBIOS": false, "PreserveBOOTCONF": true, }, nil case "x12sth-sys": return map[string]bool{ "PreserveME": false, "PreserveNVRAM": false, "PreserveSMBIOS": true, "PreserveOA": true, "PreserveSETUPCONF": true, "PreserveSETUPPWD": true, "PreserveSECBOOTKEY": true, "PreserveBOOTCONF": true, }, nil default: // ideally we never get in this position, since theres model number validation in parent callers. return nil, errors.New("unsupported model for BIOS fw install: " + c.model) } } // redfish OEM fw install parameters func (c *x12) bmcFwInstallParams() map[string]bool { return map[string]bool{ "PreserveCfg": true, "PreserveSdr": true, "PreserveSsl": true, } } func (c *x12) redfishParameters(component, targetODataID string) (*rfw.RedfishUpdateServiceParameters, error) { errUnsupported := errors.New("redfish parameters for x12 hardware component not supported: " + component) oem := OEM{} biosInstallParams, err := c.biosFwInstallParams() if err != nil { return nil, err } switch strings.ToUpper(component) { case common.SlugBIOS: oem.Supermicro.BIOS = biosInstallParams case common.SlugBMC: oem.Supermicro.BMC = c.bmcFwInstallParams() default: return nil, errUnsupported } b, err := json.Marshal(oem) if err != nil { return nil, errors.Wrap(err, "error preparing redfish parameters") } return &rfw.RedfishUpdateServiceParameters{ // NOTE: // X12s support the OnReset Apply time for BIOS updates if we want to implement that in the future. OperationApplyTime: constants.OnStartUpdateRequest, Targets: []string{targetODataID}, Oem: b, }, nil } func (c *x12) redfishOdataID(ctx context.Context, component string) (string, error) { errUnsupported := errors.New("unable to return redfish OData ID for unsupported component: " + component) switch strings.ToUpper(component) { case common.SlugBMC: return c.redfish.ManagerOdataID(ctx) case common.SlugBIOS: // hardcoded since SMCs without the DCMS license will throw license errors return "/redfish/v1/Systems/1/Bios", nil //return c.redfish.SystemsBIOSOdataID(ctx) } return "", errUnsupported } func (c *x12) firmwareInstallUploaded(ctx context.Context, component, uploadTaskID string) (installTaskID string, err error) { if err = c.supportsInstall(component); err != nil { return "", err } task, err := c.redfish.Task(ctx, uploadTaskID) if err != nil { e := fmt.Sprintf("error querying redfish tasks for firmware upload taskID: %s, err: %s", uploadTaskID, err.Error()) return "", errors.Wrap(brrs.ErrFirmwareVerifyTask, e) } taskInfo := fmt.Sprintf("id: %s, state: %s, status: %s", task.ID, task.TaskState, task.TaskStatus) if task.TaskState != schemas.CompletedTaskState { return "", errors.Wrap(brrs.ErrFirmwareVerifyTask, taskInfo) } if task.TaskStatus != "OK" { return "", errors.Wrap(brrs.ErrFirmwareVerifyTask, taskInfo) } return c.redfish.StartUpdateForUploadedFirmware(ctx) } func (c *x12) firmwareTaskStatus(ctx context.Context, component, taskID string) (state constants.TaskState, status string, err error) { if err = c.supportsInstall(component); err != nil { return "", "", errors.Wrap(brrs.ErrFirmwareTaskStatus, err.Error()) } return c.redfish.TaskStatus(ctx, taskID) } func (c *x12) getBootProgress() (*schemas.BootProgress, error) { bps, err := c.redfish.GetBootProgress() if err != nil { return nil, err } return bps[0], nil } // this is some syntactic sugar to avoid having to code potentially provider- or model-specific knowledge into a caller func (c *x12) bootComplete() (bool, error) { bp, err := c.getBootProgress() if err != nil { return false, err } // we determined this by experiment on X12STH-SYS with redfish 1.14.0 return bp.LastState == schemas.SystemHardwareInitializationCompleteBootProgressTypes, nil }