├── .circleci └── config.yml ├── .github ├── dependabot.yml └── workflows │ ├── container_description.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── .promu.yml ├── .yamllint ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── Makefile.common ├── NOTICE ├── README.md ├── SECURITY.md ├── VERSION ├── collector.go ├── collector_bmc.go ├── collector_bmc_native.go ├── collector_bmc_watchdog.go ├── collector_bmc_watchdog_native.go ├── collector_chassis.go ├── collector_chassis_native.go ├── collector_dcmi.go ├── collector_dcmi_native.go ├── collector_ipmi.go ├── collector_ipmi_native.go ├── collector_sel.go ├── collector_sel_events.go ├── collector_sel_events_native.go ├── collector_sel_native.go ├── collector_sm_lan_mode.go ├── collector_sm_lan_mode_native.go ├── config.go ├── contrib └── rpm │ ├── README.md │ ├── build.sh │ ├── config │ └── prometheus-ipmi-exporter.yml │ ├── docker │ └── Dockerfile-centos7 │ ├── prometheus-ipmi-exporter.spec │ ├── sudoers │ └── prometheus-ipmi-exporter │ └── systemd │ └── prometheus-ipmi-exporter.service ├── docker-compose.yml ├── docs ├── configuration.md ├── metrics.md ├── native.md └── privileges.md ├── freeipmi └── freeipmi.go ├── go.mod ├── go.sum ├── ipmi_local.yml ├── ipmi_local_sudo.yml ├── ipmi_remote.yml └── main.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | orbs: 4 | prometheus: prometheus/prometheus@0.17.1 5 | executors: 6 | # Whenever the Go version is updated here, .promu.yml should 7 | # also be updated. 8 | golang: 9 | docker: 10 | - image: cimg/go:1.23 11 | jobs: 12 | test: 13 | executor: golang 14 | steps: 15 | - prometheus/setup_environment 16 | - run: make 17 | - prometheus/store_artifact: 18 | file: ipmi_exporter 19 | workflows: 20 | version: 2 21 | ipmi_exporter: 22 | jobs: 23 | - test: 24 | filters: 25 | tags: 26 | only: /.*/ 27 | - prometheus/build: 28 | name: build 29 | filters: 30 | tags: 31 | only: /.*/ 32 | - prometheus/publish_master: 33 | context: org-context 34 | docker_hub_organization: prometheuscommunity 35 | quay_io_organization: prometheuscommunity 36 | requires: 37 | - test 38 | - build 39 | filters: 40 | branches: 41 | only: master 42 | - prometheus/publish_release: 43 | context: org-context 44 | docker_hub_organization: prometheuscommunity 45 | quay_io_organization: prometheuscommunity 46 | requires: 47 | - test 48 | - build 49 | filters: 50 | tags: 51 | only: /^v.*/ 52 | branches: 53 | ignore: /.*/ 54 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/container_description.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Push README to Docker Hub 3 | on: 4 | push: 5 | paths: 6 | - "README.md" 7 | - "README-containers.md" 8 | - ".github/workflows/container_description.yml" 9 | branches: [ main, master ] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | PushDockerHubReadme: 16 | runs-on: ubuntu-latest 17 | name: Push README to Docker Hub 18 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 19 | steps: 20 | - name: git checkout 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - name: Set docker hub repo name 23 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 24 | - name: Push README to Dockerhub 25 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 26 | env: 27 | DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} 28 | DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }} 29 | with: 30 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 31 | provider: dockerhub 32 | short_description: ${{ env.DOCKER_REPO_NAME }} 33 | # Empty string results in README-containers.md being pushed if it 34 | # exists. Otherwise, README.md is pushed. 35 | readme_file: '' 36 | 37 | PushQuayIoReadme: 38 | runs-on: ubuntu-latest 39 | name: Push README to quay.io 40 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 41 | steps: 42 | - name: git checkout 43 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 44 | - name: Set quay.io org name 45 | run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV 46 | - name: Set quay.io repo name 47 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 48 | - name: Push README to quay.io 49 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 50 | env: 51 | DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} 52 | with: 53 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 54 | provider: quay 55 | # Empty string results in README-containers.md being pushed if it 56 | # exists. Otherwise, README.md is pushed. 57 | readme_file: '' 58 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is synced from https://github.com/prometheus/prometheus 3 | name: golangci-lint 4 | on: 5 | push: 6 | paths: 7 | - "go.sum" 8 | - "go.mod" 9 | - "**.go" 10 | - "scripts/errcheck_excludes.txt" 11 | - ".github/workflows/golangci-lint.yml" 12 | - ".golangci.yml" 13 | pull_request: 14 | 15 | permissions: # added using https://github.com/step-security/secure-repo 16 | contents: read 17 | 18 | jobs: 19 | golangci: 20 | permissions: 21 | contents: read # for actions/checkout to fetch code 22 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 23 | name: lint 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | - name: Install Go 29 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 30 | with: 31 | go-version: 1.23.x 32 | - name: Install snmp_exporter/generator dependencies 33 | run: sudo apt-get update && sudo apt-get -y install libsnmp-dev 34 | if: github.repository == 'prometheus/snmp_exporter' 35 | - name: Lint 36 | uses: golangci/golangci-lint-action@ec5d18412c0aeab7936cb16880d708ba2a64e1ae # v6.2.0 37 | with: 38 | args: --verbose 39 | version: v1.63.4 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | ipmi_exporter 3 | *.tar.gz 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | enable: 4 | - misspell 5 | - revive 6 | - sloglint 7 | 8 | issues: 9 | exclude-rules: 10 | - path: _test.go 11 | linters: 12 | - errcheck 13 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | # Whenever the Go version is updated here, 3 | # .circleci/config.yml should also be updated. 4 | version: 1.23 5 | 6 | repository: 7 | path: github.com/prometheus-community/ipmi_exporter 8 | build: 9 | flags: -a -tags 'netgo static_build' 10 | ldflags: | 11 | -X github.com/prometheus/common/version.Version={{.Version}} 12 | -X github.com/prometheus/common/version.Revision={{.Revision}} 13 | -X github.com/prometheus/common/version.Branch={{.Branch}} 14 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 15 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 16 | tarball: 17 | files: 18 | - LICENSE 19 | crossbuild: 20 | platforms: 21 | # Disable aix: undefined: syscall.Mkfifo 22 | # - aix 23 | - darwin 24 | - dragonfly 25 | - freebsd 26 | # Disable illumos: undefined: syscall.Mkfifo 27 | # - illumos 28 | - linux 29 | - netbsd 30 | - openbsd 31 | # Disable windows: undefined: syscall.Mkfifo 32 | # - windows 33 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | ignore: | 4 | **/node_modules 5 | 6 | rules: 7 | braces: 8 | max-spaces-inside: 1 9 | level: error 10 | brackets: 11 | max-spaces-inside: 1 12 | level: error 13 | commas: disable 14 | comments: disable 15 | comments-indentation: disable 16 | document-start: disable 17 | indentation: 18 | spaces: consistent 19 | indent-sequences: consistent 20 | key-duplicates: 21 | ignore: | 22 | config/testdata/section_key_dup.bad.yml 23 | line-length: disable 24 | truthy: 25 | check-keys: false 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## next 2 | 3 | ## 1.10.0 / 2025-02-12 4 | 5 | * Adopt slog, drop go-kit/log (#210) 6 | * Add bmc url (#219) 7 | * Use long option for quiet cache (#224) 8 | * Support for Go-native IPMI (#237) 9 | * Add version collector metric (#240) 10 | * Various dependency updates and CI improvements 11 | 12 | ## 1.9.0 / 2024-10-17 13 | 14 | * Bring back aarch64 builds (#186) 15 | * Ignore time parse error in SEL events (#198) 16 | * Don't prepend to already absolute path from config (#199) 17 | * Various dependency updates 18 | 19 | ## 1.8.0 / 2024-01-23 20 | 21 | * Added BMC watchdog collector (#176) 22 | * Added SEL event metrics collector (#179) 23 | * Various dependency updates 24 | 25 | ## 1.7.0 / 2023-10-18 26 | 27 | * Update common files 28 | * Update build 29 | * Update golang to 1.21 30 | * Update dependencies 31 | * Switch to Alpine-based Docker image 32 | * Add missing error handling 33 | * Added chassis cooling fault and drive fault metrics 34 | * Now, `ipmi_dcmi_power_consumption_watts` metric is not present if Power 35 | Measurement feature is not present. Before this change - the value was zero 36 | 37 | ## 1.6.1 / 2022-06-17 38 | 39 | * Another "I screwed up the release" release 40 | 41 | ## 1.6.0 / 2022-06-17 42 | 43 | * Many improvements in underlying Prometheus libraries 44 | * Make sure `ipmimonitoring` outputs the sensor state 45 | 46 | ## 1.5.2 / 2022-03-14 47 | 48 | * Base docker images on debian/bullseye-slim 49 | * Update common files 50 | 51 | ## 1.5.1 / 2022-02-21 52 | 53 | * Bugfix release for the release process itself :) 54 | 55 | ## 1.5.0 / 2022-02-21 56 | 57 | * move to prometheus-community 58 | * new build system and (hopefully) the docker namespace 59 | * some fan sensors that measure in "percent of maximum rotation speed" now show 60 | up as fans (previously generic sensors) 61 | 62 | Thanks a lot to all the contributors and sorry for the long wait! 63 | 64 | ## 1.4.0 / 2021-06-01 65 | 66 | * Includes a lot of refactoring under the hood 67 | * Add ability to customize the commands executed by the collectors - see the sample config for some examples. 68 | 69 | ## 1.3.2 / 2021-02-22 70 | 71 | * Fixes in the `bmc` collector for systems which do not support retrieving the system firmware revision (see #57) 72 | * Fix for sensors returning multiple events formatted as a string with questionable quoting (see #62) 73 | * Use latest go builder container for the Docker image 74 | 75 | ## 1.3.1 / 2020-10-22 76 | 77 | * Fix #57 - that's all :slightly_smiling_face: 78 | 79 | ## 1.3.0 / 2020-07-26 80 | 81 | * New `sm-lan-mode` collector to get the ["LAN mode" setting](https://www.supermicro.com/support/faqs/faq.cfm?faq=28159) on Supermicro BMCs (not enabled by default) 82 | * Added "system firmware version" (i.e. the host's BIOS version) to the BMC info metric 83 | * Update all dependencies 84 | 85 | ## 1.2.0 / 2020-04-22 86 | 87 | * New `sel` collector to get number of SEL entries and free space 88 | * Update all dependencies 89 | 90 | ## 1.1.0 / 2020-02-14 91 | 92 | * Added config option for FreeIPMI workaround-flags 93 | * Added missing documentation bits around `ipmi-chassis` usage 94 | * Updated dependencies to latest version 95 | 96 | ## 1.0.0 / 2019-10-18 97 | 98 | Initial release. 99 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Prometheus Community Code of Conduct 2 | 3 | Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to ipmi_exporter 2 | 3 | In the spirit of [free software][free-sw], **everyone** is encouraged to help 4 | improve this project. Here are some ways that *you* can contribute: 5 | 6 | * Use alpha, beta, and pre-released software versions. 7 | * Report bugs. 8 | * Suggest new features. 9 | * Write or edit documentation. 10 | * Write specifications. 11 | * Write code; **no patch is too small**: fix typos, add comments, add tests, 12 | clean up inconsistent whitespace. 13 | * Refactor code. 14 | * Fix [issues][]. 15 | * Review patches. 16 | 17 | ## Submitting an issue 18 | 19 | We use the [GitHub issue tracker][issues] to track bugs and features. Before 20 | you submit a bug report or feature request, check to make sure that it has not 21 | already been submitted. When you submit a bug report, include a [Gist][] that 22 | includes a stack trace and any details that might be necessary to reproduce the 23 | bug. 24 | 25 | ## Submitting a pull request 26 | 27 | 1. [Fork the repository][fork]. 28 | 2. [Create a topic branch][branch]. 29 | 3. Implement your feature or bug fix. 30 | 4. Unlike we did so far, maybe add tests and make sure that they completely 31 | cover your changes and potential edge cases. 32 | 5. If there are tests now, run `go test`. If your tests fail, revise your code 33 | and tests, and rerun `go test` until they pass. 34 | 6. Add documentation for your feature or bug fix in the code, documentation, or 35 | PR/commit message. 36 | 7. Commit and push your changes. 37 | 8. [Submit a pull request][pr] that includes a link to the [issues][] for which 38 | you are submitting a bug fix (if applicable). 39 | 40 | 41 | [branch]: http://learn.github.com/p/branching.html 42 | [fork]: http://help.github.com/fork-a-repo 43 | [free-sw]: http://www.fsf.org/licensing/essays/free-sw.html 44 | [gist]: https://gist.github.com 45 | [issues]: https://github.com/prometheus-community/ipmi_exporter/issues 46 | [pr]: http://help.github.com/send-pull-requests 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | FROM --platform=${OS}/${ARCH} alpine:3 4 | RUN apk --no-cache add freeipmi 5 | LABEL maintainer="The Prometheus Authors " 6 | 7 | ARG ARCH="amd64" 8 | ARG OS="linux" 9 | COPY .build/${OS}-${ARCH}/ipmi_exporter /bin/ipmi_exporter 10 | 11 | EXPOSE 9290 12 | USER nobody 13 | ENTRYPOINT [ "/bin/ipmi_exporter" ] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019-2021 SoundCloud Ltd. and the IPMI exporter developers 4 | Copyright (c) 2021 The Prometheus Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | General maintainers: 4 | * Conrad Hoffmann (ch@bitfehler.net / @bitfehler) 5 | * Ben Kochie (superq@gmail.com / @SuperQ) 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # Override the default common all. 15 | .PHONY: all 16 | all: precheck style unused build test 17 | 18 | DOCKER_ARCHS ?= amd64 arm64 19 | DOCKER_IMAGE_NAME ?= ipmi-exporter 20 | DOCKER_REPO ?= prometheuscommunity 21 | 22 | include Makefile.common 23 | -------------------------------------------------------------------------------- /Makefile.common: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | 15 | # A common Makefile that includes rules to be reused in different prometheus projects. 16 | # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! 17 | 18 | # Example usage : 19 | # Create the main Makefile in the root project directory. 20 | # include Makefile.common 21 | # customTarget: 22 | # @echo ">> Running customTarget" 23 | # 24 | 25 | # Ensure GOBIN is not set during build so that promu is installed to the correct path 26 | unexport GOBIN 27 | 28 | GO ?= go 29 | GOFMT ?= $(GO)fmt 30 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) 31 | GOOPTS ?= 32 | GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) 33 | GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) 34 | 35 | GO_VERSION ?= $(shell $(GO) version) 36 | GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) 37 | PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') 38 | 39 | PROMU := $(FIRST_GOPATH)/bin/promu 40 | pkgs = ./... 41 | 42 | ifeq (arm, $(GOHOSTARCH)) 43 | GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) 44 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) 45 | else 46 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) 47 | endif 48 | 49 | GOTEST := $(GO) test 50 | GOTEST_DIR := 51 | ifneq ($(CIRCLE_JOB),) 52 | ifneq ($(shell command -v gotestsum 2> /dev/null),) 53 | GOTEST_DIR := test-results 54 | GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- 55 | endif 56 | endif 57 | 58 | PROMU_VERSION ?= 0.17.0 59 | PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz 60 | 61 | SKIP_GOLANGCI_LINT := 62 | GOLANGCI_LINT := 63 | GOLANGCI_LINT_OPTS ?= 64 | GOLANGCI_LINT_VERSION ?= v1.63.4 65 | # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. 66 | # windows isn't included here because of the path separator being different. 67 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) 68 | ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) 69 | # If we're in CI and there is an Actions file, that means the linter 70 | # is being run in Actions, so we don't need to run it here. 71 | ifneq (,$(SKIP_GOLANGCI_LINT)) 72 | GOLANGCI_LINT := 73 | else ifeq (,$(CIRCLE_JOB)) 74 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 75 | else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) 76 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 77 | endif 78 | endif 79 | endif 80 | 81 | PREFIX ?= $(shell pwd) 82 | BIN_DIR ?= $(shell pwd) 83 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 84 | DOCKERFILE_PATH ?= ./Dockerfile 85 | DOCKERBUILD_CONTEXT ?= ./ 86 | DOCKER_REPO ?= prom 87 | 88 | DOCKER_ARCHS ?= amd64 89 | 90 | BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) 91 | PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) 92 | TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) 93 | 94 | SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) 95 | 96 | ifeq ($(GOHOSTARCH),amd64) 97 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) 98 | # Only supported on amd64 99 | test-flags := -race 100 | endif 101 | endif 102 | 103 | # This rule is used to forward a target like "build" to "common-build". This 104 | # allows a new "build" target to be defined in a Makefile which includes this 105 | # one and override "common-build" without override warnings. 106 | %: common-% ; 107 | 108 | .PHONY: common-all 109 | common-all: precheck style check_license lint yamllint unused build test 110 | 111 | .PHONY: common-style 112 | common-style: 113 | @echo ">> checking code style" 114 | @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ 115 | if [ -n "$${fmtRes}" ]; then \ 116 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 117 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 118 | exit 1; \ 119 | fi 120 | 121 | .PHONY: common-check_license 122 | common-check_license: 123 | @echo ">> checking license header" 124 | @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ 125 | awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ 126 | done); \ 127 | if [ -n "$${licRes}" ]; then \ 128 | echo "license header checking failed:"; echo "$${licRes}"; \ 129 | exit 1; \ 130 | fi 131 | 132 | .PHONY: common-deps 133 | common-deps: 134 | @echo ">> getting dependencies" 135 | $(GO) mod download 136 | 137 | .PHONY: update-go-deps 138 | update-go-deps: 139 | @echo ">> updating Go dependencies" 140 | @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ 141 | $(GO) get -d $$m; \ 142 | done 143 | $(GO) mod tidy 144 | 145 | .PHONY: common-test-short 146 | common-test-short: $(GOTEST_DIR) 147 | @echo ">> running short tests" 148 | $(GOTEST) -short $(GOOPTS) $(pkgs) 149 | 150 | .PHONY: common-test 151 | common-test: $(GOTEST_DIR) 152 | @echo ">> running all tests" 153 | $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) 154 | 155 | $(GOTEST_DIR): 156 | @mkdir -p $@ 157 | 158 | .PHONY: common-format 159 | common-format: 160 | @echo ">> formatting code" 161 | $(GO) fmt $(pkgs) 162 | 163 | .PHONY: common-vet 164 | common-vet: 165 | @echo ">> vetting code" 166 | $(GO) vet $(GOOPTS) $(pkgs) 167 | 168 | .PHONY: common-lint 169 | common-lint: $(GOLANGCI_LINT) 170 | ifdef GOLANGCI_LINT 171 | @echo ">> running golangci-lint" 172 | $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) 173 | endif 174 | 175 | .PHONY: common-lint-fix 176 | common-lint-fix: $(GOLANGCI_LINT) 177 | ifdef GOLANGCI_LINT 178 | @echo ">> running golangci-lint fix" 179 | $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) 180 | endif 181 | 182 | .PHONY: common-yamllint 183 | common-yamllint: 184 | @echo ">> running yamllint on all YAML files in the repository" 185 | ifeq (, $(shell command -v yamllint 2> /dev/null)) 186 | @echo "yamllint not installed so skipping" 187 | else 188 | yamllint . 189 | endif 190 | 191 | # For backward-compatibility. 192 | .PHONY: common-staticcheck 193 | common-staticcheck: lint 194 | 195 | .PHONY: common-unused 196 | common-unused: 197 | @echo ">> running check for unused/missing packages in go.mod" 198 | $(GO) mod tidy 199 | @git diff --exit-code -- go.sum go.mod 200 | 201 | .PHONY: common-build 202 | common-build: promu 203 | @echo ">> building binaries" 204 | $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) 205 | 206 | .PHONY: common-tarball 207 | common-tarball: promu 208 | @echo ">> building release tarball" 209 | $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 210 | 211 | .PHONY: common-docker-repo-name 212 | common-docker-repo-name: 213 | @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" 214 | 215 | .PHONY: common-docker $(BUILD_DOCKER_ARCHS) 216 | common-docker: $(BUILD_DOCKER_ARCHS) 217 | $(BUILD_DOCKER_ARCHS): common-docker-%: 218 | docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ 219 | -f $(DOCKERFILE_PATH) \ 220 | --build-arg ARCH="$*" \ 221 | --build-arg OS="linux" \ 222 | $(DOCKERBUILD_CONTEXT) 223 | 224 | .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) 225 | common-docker-publish: $(PUBLISH_DOCKER_ARCHS) 226 | $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: 227 | docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" 228 | 229 | DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) 230 | .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) 231 | common-docker-tag-latest: $(TAG_DOCKER_ARCHS) 232 | $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: 233 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" 234 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" 235 | 236 | .PHONY: common-docker-manifest 237 | common-docker-manifest: 238 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG)) 239 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" 240 | 241 | .PHONY: promu 242 | promu: $(PROMU) 243 | 244 | $(PROMU): 245 | $(eval PROMU_TMP := $(shell mktemp -d)) 246 | curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) 247 | mkdir -p $(FIRST_GOPATH)/bin 248 | cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu 249 | rm -r $(PROMU_TMP) 250 | 251 | .PHONY: proto 252 | proto: 253 | @echo ">> generating code from proto files" 254 | @./scripts/genproto.sh 255 | 256 | ifdef GOLANGCI_LINT 257 | $(GOLANGCI_LINT): 258 | mkdir -p $(FIRST_GOPATH)/bin 259 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ 260 | | sed -e '/install -d/d' \ 261 | | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) 262 | endif 263 | 264 | .PHONY: precheck 265 | precheck:: 266 | 267 | define PRECHECK_COMMAND_template = 268 | precheck:: $(1)_precheck 269 | 270 | PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) 271 | .PHONY: $(1)_precheck 272 | $(1)_precheck: 273 | @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ 274 | echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ 275 | exit 1; \ 276 | fi 277 | endef 278 | 279 | govulncheck: install-govulncheck 280 | govulncheck ./... 281 | 282 | install-govulncheck: 283 | command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest 284 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | IPMI Exporter 2 | 3 | Copyright 2019-2021 SoundCloud Ltd. and the IPMI exporter developers 4 | 5 | Copyright 2021 The Prometheus Authors 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Prometheus IPMI Exporter 2 | ======================== 3 | 4 | [![Build Status](https://circleci.com/gh/prometheus-community/ipmi_exporter.svg?style=svg)](https://circleci.com/gh/prometheus-community/ipmi_exporter) 5 | 6 | This is an IPMI exporter for [Prometheus][prometheus]. 7 | 8 | [prometheus]: https://prometheus.io "Prometheus homepage" 9 | 10 | It supports both the regular `/metrics` endpoint, exposing metrics from the 11 | host that the exporter is running on, as well as an `/ipmi` endpoint that 12 | supports IPMI over RMCP, implementing the multi-target exporter pattern. If you 13 | plan to use the latter, please read the guide [Understanding and using the 14 | multi-target exporter pattern][multi-target] to get the general idea about the 15 | configuration. 16 | 17 | [multi-target]: https://prometheus.io/docs/guides/multi-target-exporter/ 18 | 19 | By default, the exporter relies on tools from the [FreeIPMI][freeipmi] suite 20 | for the actual IPMI implementation. 21 | 22 | [freeipmi]: https://www.gnu.org/software/freeipmi/ "FreeIPMI homepage" 23 | 24 | There is, however, experimental support for using the Go-native [go-ipmi 25 | library](https://github.com/bougou/go-ipmi/) instead of FreeIPMI. Feedback to 26 | help mature this support would be greatly appreciated. Please read the [native 27 | IPMI documentation](docs/native.md) if you are interested. 28 | 29 | ## Installation 30 | 31 | For most use-cases, simply download the [the latest release][releases]. 32 | 33 | [releases]: https://github.com/prometheus-community/ipmi_exporter/releases "IPMI exporter releases on Github" 34 | 35 | For Kubernets, you can use the community-maintained [Helm chart][helm]. 36 | 37 | [helm]: https://github.com/prometheus-community/helm-charts/tree/main/charts/prometheus-ipmi-exporter "IPMI exporter Helm chart in the helm-charts Github repo" 38 | 39 | Pre-built container images are available on [dockerhub][dockerhub] and 40 | [quay.io][quay.io]. 41 | 42 | [dockerhub]: https://hub.docker.com/r/prometheuscommunity/ipmi-exporter 43 | [quay.io]: https://quay.io/repository/prometheuscommunity/ipmi-exporter 44 | 45 | ### Building from source 46 | 47 | You need a Go development environment. Then, simply run `make` to build the 48 | executable: 49 | 50 | make 51 | 52 | This uses the common prometheus tooling to build and run some tests. 53 | 54 | Alternatively, you can use the standard Go tooling, which will install the 55 | executable in `$GOPATH/bin`: 56 | 57 | go install github.com/prometheus-community/ipmi_exporter@latest 58 | 59 | ### Building a container image 60 | 61 | You can build a container image with the included `docker` make target: 62 | 63 | make promu 64 | promu crossbuild -p linux/amd64 -p linux/arm64 65 | make docker 66 | 67 | ## Running 68 | 69 | A minimal invocation looks like this: 70 | 71 | ./ipmi_exporter 72 | 73 | Supported parameters include: 74 | 75 | - `web.listen-address`: the address/port to listen on (default: `":9290"`) 76 | - `config.file`: path to the configuration file (default: none) 77 | - `freeipmi.path`: path to the FreeIPMI executables (default: rely on `$PATH`) 78 | 79 | For syntax and a complete list of available parameters, run: 80 | 81 | ./ipmi_exporter -h 82 | 83 | Make sure you have the following tools from the [FreeIPMI][freeipmi] suite 84 | installed: 85 | 86 | - `ipmimonitoring`/`ipmi-sensors` 87 | - `ipmi-dcmi` 88 | - `ipmi-raw` 89 | - `bmc-info` 90 | - `ipmi-sel` 91 | - `ipmi-chassis` 92 | 93 | When running a container image, make sure to: 94 | 95 | - set `config.file` to where the config file is mounted 96 | - expose the default port (9290) or set `web.listen-address` accordingly 97 | 98 | **NOTE:** you should only use containers for collecting remote metrics. 99 | 100 | ## Configuration 101 | 102 | The [configuration](docs/configuration.md) document describes both the 103 | configuration of the IPMI exporter itself as well as providing some guidance 104 | for configuring the Prometheus server to scrape it. 105 | 106 | ## TLS and basic authentication 107 | 108 | The IPMI Exporter supports TLS and basic authentication. 109 | 110 | To use TLS and/or basic authentication, you need to pass a configuration file 111 | using the `--web.config.file` parameter. The format of the file is described 112 | [in the exporter-toolkit repository][toolkit]. 113 | 114 | [toolkit]: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md 115 | 116 | ## Exported data 117 | 118 | For a description of the metrics that this exporter provides, see the 119 | [metrics](docs/metrics.md) document. 120 | 121 | ## Privileges 122 | 123 | Collecting host-local IPMI metrics requires root privileges. See 124 | [privileges](docs/privileges.md) document for how to avoid running the exporter 125 | as root. 126 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a security issue 2 | 3 | The Prometheus security policy, including how to report vulnerabilities, can be 4 | found here: 5 | 6 | 7 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.10.0 2 | -------------------------------------------------------------------------------- /collector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | "path" 19 | "strings" 20 | "time" 21 | 22 | "github.com/bougou/go-ipmi" 23 | "github.com/prometheus/client_golang/prometheus" 24 | 25 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 26 | ) 27 | 28 | const ( 29 | namespace = "ipmi" 30 | targetLocal = "" 31 | ) 32 | 33 | type collector interface { 34 | Name() CollectorName 35 | Cmd() string 36 | Args() []string 37 | Collect(output freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) 38 | } 39 | 40 | type metaCollector struct { 41 | target string 42 | module string 43 | config *SafeConfig 44 | } 45 | 46 | type ipmiTarget struct { 47 | host string 48 | config IPMIConfig 49 | } 50 | 51 | var ( 52 | upDesc = prometheus.NewDesc( 53 | prometheus.BuildFQName(namespace, "", "up"), 54 | "'1' if a scrape of the IPMI device was successful, '0' otherwise.", 55 | []string{"collector"}, 56 | nil, 57 | ) 58 | 59 | durationDesc = prometheus.NewDesc( 60 | prometheus.BuildFQName(namespace, "scrape_duration", "seconds"), 61 | "Returns how long the scrape took to complete in seconds.", 62 | nil, 63 | nil, 64 | ) 65 | ) 66 | 67 | // Describe implements Prometheus.Collector. 68 | func (c metaCollector) Describe(_ chan<- *prometheus.Desc) { 69 | // all metrics are described ad-hoc 70 | } 71 | 72 | func markCollectorUp(ch chan<- prometheus.Metric, name string, up int) { 73 | ch <- prometheus.MustNewConstMetric( 74 | upDesc, 75 | prometheus.GaugeValue, 76 | float64(up), 77 | name, 78 | ) 79 | } 80 | 81 | // Collect implements Prometheus.Collector. 82 | func (c metaCollector) Collect(ch chan<- prometheus.Metric) { 83 | start := time.Now() 84 | defer func() { 85 | duration := time.Since(start).Seconds() 86 | logger.Debug("Scrape duration", "target", targetName(c.target), "duration", duration) 87 | ch <- prometheus.MustNewConstMetric( 88 | durationDesc, 89 | prometheus.GaugeValue, 90 | duration, 91 | ) 92 | }() 93 | 94 | config := c.config.ConfigForTarget(c.target, c.module) 95 | target := ipmiTarget{ 96 | host: c.target, 97 | config: config, 98 | } 99 | 100 | for _, collector := range config.GetCollectors() { 101 | var up int 102 | logger.Debug("Running collector", "target", target.host, "collector", collector.Name()) 103 | 104 | fqcmd := collector.Cmd() 105 | result := freeipmi.Result{} 106 | 107 | // Go-native collectors return empty string as command 108 | if fqcmd != "" { 109 | if !path.IsAbs(fqcmd) { 110 | fqcmd = path.Join(*executablesPath, collector.Cmd()) 111 | } 112 | args := collector.Args() 113 | cfg := config.GetFreeipmiConfig() 114 | 115 | result = freeipmi.Execute(fqcmd, args, cfg, target.host, logger) 116 | } 117 | 118 | up, err := collector.Collect(result, ch, target) 119 | if err != nil { 120 | logger.Error("Collector failed", "name", collector.Name(), "error", err) 121 | } 122 | markCollectorUp(ch, string(collector.Name()), up) 123 | } 124 | } 125 | 126 | func targetName(target string) string { 127 | if target == targetLocal { 128 | return "[local]" 129 | } 130 | return target 131 | } 132 | 133 | func NewNativeClient(target ipmiTarget) (*ipmi.Client, error) { 134 | var client *ipmi.Client 135 | var err error 136 | 137 | if target.host == targetLocal { 138 | client, err = ipmi.NewOpenClient() 139 | } else { 140 | client, err = ipmi.NewClient(target.host, 623, target.config.User, target.config.Password) 141 | } 142 | if err != nil { 143 | logger.Error("Error creating IPMI client", "target", target.host, "error", err) 144 | return nil, err 145 | } 146 | if target.host != targetLocal { 147 | // TODO it's probably safe to ditch other interfaces? 148 | client = client.WithInterface(ipmi.InterfaceLanplus) 149 | } 150 | if target.config.Timeout != 0 { 151 | client = client.WithTimeout(time.Duration(target.config.Timeout * uint32(time.Millisecond))) 152 | } 153 | if target.config.Privilege != "" { 154 | // TODO this means different default (unspecified) for native vs. FreeIPMI (operator) 155 | priv := ipmi.PrivilegeLevelUnspecified 156 | switch strings.ToLower(target.config.Privilege) { 157 | case "admin": 158 | priv = ipmi.PrivilegeLevelAdministrator 159 | case "operator": 160 | priv = ipmi.PrivilegeLevelOperator 161 | case "user": 162 | priv = ipmi.PrivilegeLevelUser 163 | } 164 | client = client.WithMaxPrivilegeLevel(priv) 165 | } 166 | // TODO workaround-flags not used in native client 167 | if err := client.Connect(context.TODO()); err != nil { 168 | logger.Error("Error connecting to IPMI device", "target", target.host, "error", err) 169 | return nil, err 170 | } 171 | return client, nil 172 | } 173 | -------------------------------------------------------------------------------- /collector_bmc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "github.com/prometheus/client_golang/prometheus" 18 | 19 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 20 | ) 21 | 22 | const ( 23 | BMCCollectorName CollectorName = "bmc" 24 | ) 25 | 26 | var ( 27 | bmcInfoDesc = prometheus.NewDesc( 28 | prometheus.BuildFQName(namespace, "bmc", "info"), 29 | "Constant metric with value '1' providing details about the BMC.", 30 | []string{"firmware_revision", "manufacturer_id", "system_firmware_version", "bmc_url"}, 31 | nil, 32 | ) 33 | ) 34 | 35 | type BMCCollector struct{} 36 | 37 | func (c BMCCollector) Name() CollectorName { 38 | return BMCCollectorName 39 | } 40 | 41 | func (c BMCCollector) Cmd() string { 42 | return "bmc-info" 43 | } 44 | 45 | func (c BMCCollector) Args() []string { 46 | return []string{} 47 | } 48 | 49 | func (c BMCCollector) Collect(result freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 50 | firmwareRevision, err := freeipmi.GetBMCInfoFirmwareRevision(result) 51 | if err != nil { 52 | logger.Error("Failed to collect BMC data", "target", targetName(target.host), "error", err) 53 | return 0, err 54 | } 55 | manufacturerID, err := freeipmi.GetBMCInfoManufacturerID(result) 56 | if err != nil { 57 | logger.Error("Failed to collect BMC data", "target", targetName(target.host), "error", err) 58 | return 0, err 59 | } 60 | systemFirmwareVersion, err := freeipmi.GetBMCInfoSystemFirmwareVersion(result) 61 | if err != nil { 62 | // This one is not always available. 63 | logger.Debug("Failed to parse bmc-info data", "target", targetName(target.host), "error", err) 64 | systemFirmwareVersion = "N/A" 65 | } 66 | bmcURL, err := freeipmi.GetBMCInfoBmcURL(result) 67 | if err != nil { 68 | // This one is not always available. 69 | logger.Debug("Failed to parse bmc-info data", "target", targetName(target.host), "error", err) 70 | bmcURL = "N/A" 71 | } 72 | ch <- prometheus.MustNewConstMetric( 73 | bmcInfoDesc, 74 | prometheus.GaugeValue, 75 | 1, 76 | firmwareRevision, manufacturerID, systemFirmwareVersion, bmcURL, 77 | ) 78 | return 1, nil 79 | } 80 | -------------------------------------------------------------------------------- /collector_bmc_native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | "strconv" 19 | 20 | "github.com/bougou/go-ipmi" 21 | "github.com/prometheus/client_golang/prometheus" 22 | 23 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 24 | ) 25 | 26 | var ( 27 | bmcNativeInfoDesc = prometheus.NewDesc( 28 | prometheus.BuildFQName(namespace, "bmc", "info"), 29 | "Constant metric with value '1' providing details about the BMC.", 30 | []string{"firmware_revision", "manufacturer", "manufacturer_id", "system_firmware_version"}, 31 | nil, 32 | ) 33 | ) 34 | 35 | type BMCNativeCollector struct{} 36 | 37 | func (c BMCNativeCollector) Name() CollectorName { 38 | // The name is intentionally the same as the non-native collector 39 | return BMCCollectorName 40 | } 41 | 42 | func (c BMCNativeCollector) Cmd() string { 43 | return "" // native collector => empty command 44 | } 45 | 46 | func (c BMCNativeCollector) Args() []string { 47 | return []string{} 48 | } 49 | 50 | func (c BMCNativeCollector) Collect(_ freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 51 | client, err := NewNativeClient(target) 52 | if err != nil { 53 | return 0, err 54 | } 55 | res, err := client.GetDeviceID(context.TODO()) 56 | if err != nil { 57 | return 0, err 58 | } 59 | 60 | // The API looks slightly awkward here, but doing this instead of calling 61 | // client.GetSystemInfo() greatly reduces the number of required round-trips. 62 | systemInfo := ipmi.SystemInfoParams{ 63 | SystemFirmwareVersions: make([]*ipmi.SystemInfoParam_SystemFirmwareVersion, 0), 64 | } 65 | err = client.GetSystemInfoParamsFor(context.TODO(), &systemInfo) 66 | // This one is not always available 67 | systemFirmwareVersion := "N/A" 68 | if err != nil { 69 | logger.Debug("Failed to get system firmware version", "target", targetName(target.host), "error", err) 70 | } else { 71 | systemFirmwareVersion = systemInfo.ToSystemInfo().SystemFirmwareVersion 72 | } 73 | 74 | ch <- prometheus.MustNewConstMetric( 75 | bmcNativeInfoDesc, 76 | prometheus.GaugeValue, 77 | 1, 78 | res.FirmwareVersionStr(), 79 | ipmi.OEM(res.ManufacturerID).String(), 80 | strconv.FormatUint(uint64(res.ManufacturerID), 10), 81 | systemFirmwareVersion, 82 | ) 83 | return 1, nil 84 | } 85 | -------------------------------------------------------------------------------- /collector_bmc_watchdog.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "github.com/prometheus/client_golang/prometheus" 18 | 19 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 20 | ) 21 | 22 | const ( 23 | BMCWatchdogCollectorName CollectorName = "bmc-watchdog" 24 | ) 25 | 26 | var ( 27 | bmcWatchdogTimerDesc = prometheus.NewDesc( 28 | prometheus.BuildFQName(namespace, "bmc_watchdog", "timer_state"), 29 | "Watchdog timer running (1: running, 0: stopped)", 30 | []string{}, 31 | nil, 32 | ) 33 | watchdogTimerUses = []string{"BIOS FRB2", "BIOS POST", "OS LOAD", "SMS/OS", "OEM"} 34 | bmcWatchdogTimerUseDesc = prometheus.NewDesc( 35 | prometheus.BuildFQName(namespace, "bmc_watchdog", "timer_use_state"), 36 | "Watchdog timer use (1: active, 0: inactive)", 37 | []string{"name"}, 38 | nil, 39 | ) 40 | bmcWatchdogLoggingDesc = prometheus.NewDesc( 41 | prometheus.BuildFQName(namespace, "bmc_watchdog", "logging_state"), 42 | "Watchdog log flag (1: Enabled, 0: Disabled / note: reverse of freeipmi)", 43 | []string{}, 44 | nil, 45 | ) 46 | watchdogTimeoutActions = []string{"None", "Hard Reset", "Power Down", "Power Cycle"} 47 | bmcWatchdogTimeoutActionDesc = prometheus.NewDesc( 48 | prometheus.BuildFQName(namespace, "bmc_watchdog", "timeout_action_state"), 49 | "Watchdog timeout action (1: active, 0: inactive)", 50 | []string{"action"}, 51 | nil, 52 | ) 53 | watchdogPretimeoutInterrupts = []string{"None", "SMI", "NMI / Diagnostic Interrupt", "Messaging Interrupt"} 54 | bmcWatchdogPretimeoutInterruptDesc = prometheus.NewDesc( 55 | prometheus.BuildFQName(namespace, "bmc_watchdog", "pretimeout_interrupt_state"), 56 | "Watchdog pre-timeout interrupt (1: active, 0: inactive)", 57 | []string{"interrupt"}, 58 | nil, 59 | ) 60 | bmcWatchdogPretimeoutIntervalDesc = prometheus.NewDesc( 61 | prometheus.BuildFQName(namespace, "bmc_watchdog", "pretimeout_interval_seconds"), 62 | "Watchdog pre-timeout interval in seconds", 63 | []string{}, 64 | nil, 65 | ) 66 | bmcWatchdogInitialCountdownDesc = prometheus.NewDesc( 67 | prometheus.BuildFQName(namespace, "bmc_watchdog", "initial_countdown_seconds"), 68 | "Watchdog initial countdown in seconds", 69 | []string{}, 70 | nil, 71 | ) 72 | bmcWatchdogCurrentCountdownDesc = prometheus.NewDesc( 73 | prometheus.BuildFQName(namespace, "bmc_watchdog", "current_countdown_seconds"), 74 | "Watchdog initial countdown in seconds", 75 | []string{}, 76 | nil, 77 | ) 78 | ) 79 | 80 | type BMCWatchdogCollector struct{} 81 | 82 | func (c BMCWatchdogCollector) Name() CollectorName { 83 | return BMCWatchdogCollectorName 84 | } 85 | 86 | func (c BMCWatchdogCollector) Cmd() string { 87 | return "bmc-watchdog" 88 | } 89 | 90 | func (c BMCWatchdogCollector) Args() []string { 91 | return []string{"--get"} 92 | } 93 | 94 | func (c BMCWatchdogCollector) Collect(result freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 95 | timerState, err := freeipmi.GetBMCWatchdogTimerState(result) 96 | if err != nil { 97 | logger.Error("Failed to collect BMC watchdog timer", "target", targetName(target.host), "error", err) 98 | return 0, err 99 | } 100 | currentTimerUse, err := freeipmi.GetBMCWatchdogTimerUse(result) 101 | if err != nil { 102 | logger.Error("Failed to collect BMC watchdog timer use", "target", targetName(target.host), "error", err) 103 | return 0, err 104 | } 105 | loggingState, err := freeipmi.GetBMCWatchdogLoggingState(result) 106 | if err != nil { 107 | logger.Error("Failed to collect BMC watchdog logging", "target", targetName(target.host), "error", err) 108 | return 0, err 109 | } 110 | currentTimeoutAction, err := freeipmi.GetBMCWatchdogTimeoutAction(result) 111 | if err != nil { 112 | logger.Error("Failed to collect BMC watchdog timeout action", "target", targetName(target.host), "error", err) 113 | return 0, err 114 | } 115 | currentPretimeoutInterrupt, err := freeipmi.GetBMCWatchdogPretimeoutInterrupt(result) 116 | if err != nil { 117 | logger.Error("Failed to collect BMC watchdog pretimeout interrupt", "target", targetName(target.host), "error", err) 118 | return 0, err 119 | } 120 | pretimeoutInterval, err := freeipmi.GetBMCWatchdogPretimeoutInterval(result) 121 | if err != nil { 122 | logger.Error("Failed to collect BMC watchdog pretimeout interval", "target", targetName(target.host), "error", err) 123 | return 0, err 124 | } 125 | initialCountdown, err := freeipmi.GetBMCWatchdogInitialCountdown(result) 126 | if err != nil { 127 | logger.Error("Failed to collect BMC watchdog initial countdown", "target", targetName(target.host), "error", err) 128 | return 0, err 129 | } 130 | currentCountdown, err := freeipmi.GetBMCWatchdogCurrentCountdown(result) 131 | if err != nil { 132 | logger.Error("Failed to collect BMC watchdog current countdown", "target", targetName(target.host), "error", err) 133 | return 0, err 134 | } 135 | 136 | ch <- prometheus.MustNewConstMetric(bmcWatchdogTimerDesc, prometheus.GaugeValue, timerState) 137 | for _, timerUse := range watchdogTimerUses { 138 | if currentTimerUse == timerUse { 139 | ch <- prometheus.MustNewConstMetric(bmcWatchdogTimerUseDesc, prometheus.GaugeValue, 1, timerUse) 140 | } else { 141 | ch <- prometheus.MustNewConstMetric(bmcWatchdogTimerUseDesc, prometheus.GaugeValue, 0, timerUse) 142 | } 143 | } 144 | ch <- prometheus.MustNewConstMetric(bmcWatchdogLoggingDesc, prometheus.GaugeValue, loggingState) 145 | for _, timeoutAction := range watchdogTimeoutActions { 146 | if currentTimeoutAction == timeoutAction { 147 | ch <- prometheus.MustNewConstMetric(bmcWatchdogTimeoutActionDesc, prometheus.GaugeValue, 1, timeoutAction) 148 | } else { 149 | ch <- prometheus.MustNewConstMetric(bmcWatchdogTimeoutActionDesc, prometheus.GaugeValue, 0, timeoutAction) 150 | } 151 | } 152 | for _, pretimeoutInterrupt := range watchdogPretimeoutInterrupts { 153 | if currentPretimeoutInterrupt == pretimeoutInterrupt { 154 | ch <- prometheus.MustNewConstMetric(bmcWatchdogPretimeoutInterruptDesc, prometheus.GaugeValue, 1, pretimeoutInterrupt) 155 | } else { 156 | ch <- prometheus.MustNewConstMetric(bmcWatchdogPretimeoutInterruptDesc, prometheus.GaugeValue, 0, pretimeoutInterrupt) 157 | } 158 | } 159 | ch <- prometheus.MustNewConstMetric(bmcWatchdogPretimeoutIntervalDesc, prometheus.GaugeValue, pretimeoutInterval) 160 | ch <- prometheus.MustNewConstMetric(bmcWatchdogInitialCountdownDesc, prometheus.GaugeValue, initialCountdown) 161 | ch <- prometheus.MustNewConstMetric(bmcWatchdogCurrentCountdownDesc, prometheus.GaugeValue, currentCountdown) 162 | return 1, nil 163 | } 164 | -------------------------------------------------------------------------------- /collector_bmc_watchdog_native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | 21 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 22 | ) 23 | 24 | var ( 25 | bmcWatchdogNativeTimerDesc = prometheus.NewDesc( 26 | prometheus.BuildFQName(namespace, "bmc_watchdog", "timer_state"), 27 | "Watchdog timer running (1: running, 0: stopped)", 28 | []string{}, 29 | nil, 30 | ) 31 | // TODO add "Reserved" (0x0)? also needed in lib 32 | watchdogNativeTimerUses = []string{"BIOS FRB2", "BIOS/POST", "OS Load", "SMS/OS", "OEM"} 33 | bmcWatchdogNativeTimerUseDesc = prometheus.NewDesc( 34 | prometheus.BuildFQName(namespace, "bmc_watchdog", "timer_use_state"), 35 | "Watchdog timer use (1: active, 0: inactive)", 36 | []string{"name"}, 37 | nil, 38 | ) 39 | bmcWatchdogNativeLoggingDesc = prometheus.NewDesc( 40 | prometheus.BuildFQName(namespace, "bmc_watchdog", "logging_state"), 41 | "Watchdog log flag (1: Enabled, 0: Disabled / note: reverse of freeipmi)", 42 | []string{}, 43 | nil, 44 | ) 45 | watchdogNativeTimeoutActions = []string{"No action", "Hard Reset", "Power Down", "Power Cycle"} 46 | bmcWatchdogNativeTimeoutActionDesc = prometheus.NewDesc( 47 | prometheus.BuildFQName(namespace, "bmc_watchdog", "timeout_action_state"), 48 | "Watchdog timeout action (1: active, 0: inactive)", 49 | []string{"action"}, 50 | nil, 51 | ) 52 | watchdogNativePretimeoutInterrupts = []string{"None", "SMI", "NMI / Diagnostic Interrupt", "Messaging Interrupt"} 53 | bmcWatchdogNativePretimeoutInterruptDesc = prometheus.NewDesc( 54 | prometheus.BuildFQName(namespace, "bmc_watchdog", "pretimeout_interrupt_state"), 55 | "Watchdog pre-timeout interrupt (1: active, 0: inactive)", 56 | []string{"interrupt"}, 57 | nil, 58 | ) 59 | bmcWatchdogNativePretimeoutIntervalDesc = prometheus.NewDesc( 60 | prometheus.BuildFQName(namespace, "bmc_watchdog", "pretimeout_interval_seconds"), 61 | "Watchdog pre-timeout interval in seconds", 62 | []string{}, 63 | nil, 64 | ) 65 | bmcWatchdogNativeInitialCountdownDesc = prometheus.NewDesc( 66 | prometheus.BuildFQName(namespace, "bmc_watchdog", "initial_countdown_seconds"), 67 | "Watchdog initial countdown in seconds", 68 | []string{}, 69 | nil, 70 | ) 71 | bmcWatchdogNativeCurrentCountdownDesc = prometheus.NewDesc( 72 | prometheus.BuildFQName(namespace, "bmc_watchdog", "current_countdown_seconds"), 73 | "Watchdog initial countdown in seconds", 74 | []string{}, 75 | nil, 76 | ) 77 | ) 78 | 79 | type BMCWatchdogNativeCollector struct{} 80 | 81 | func (c BMCWatchdogNativeCollector) Name() CollectorName { 82 | // The name is intentionally the same as the non-native collector 83 | return BMCWatchdogCollectorName 84 | } 85 | 86 | func (c BMCWatchdogNativeCollector) Cmd() string { 87 | return "" 88 | } 89 | 90 | func (c BMCWatchdogNativeCollector) Args() []string { 91 | return []string{} 92 | } 93 | 94 | func (c BMCWatchdogNativeCollector) Collect(_ freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 95 | 96 | // TODO this now works remotely 97 | 98 | client, err := NewNativeClient(target) 99 | if err != nil { 100 | return 0, err 101 | } 102 | res, err := client.GetWatchdogTimer(context.TODO()) 103 | if err != nil { 104 | return 0, err 105 | } 106 | 107 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativeTimerDesc, prometheus.GaugeValue, boolToFloat(res.TimerIsStarted)) 108 | for _, timerUse := range watchdogNativeTimerUses { 109 | if res.TimerUse.String() == timerUse { 110 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativeTimerUseDesc, prometheus.GaugeValue, 1, timerUse) 111 | } else { 112 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativeTimerUseDesc, prometheus.GaugeValue, 0, timerUse) 113 | } 114 | } 115 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativeLoggingDesc, prometheus.GaugeValue, boolToFloat(!res.DontLog)) 116 | for _, timeoutAction := range watchdogNativeTimeoutActions { 117 | if res.TimeoutAction.String() == timeoutAction { 118 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativeTimeoutActionDesc, prometheus.GaugeValue, 1, timeoutAction) 119 | } else { 120 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativeTimeoutActionDesc, prometheus.GaugeValue, 0, timeoutAction) 121 | } 122 | } 123 | for _, pretimeoutInterrupt := range watchdogNativePretimeoutInterrupts { 124 | if res.PreTimeoutInterrupt.String() == pretimeoutInterrupt { 125 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativePretimeoutInterruptDesc, prometheus.GaugeValue, 1, pretimeoutInterrupt) 126 | } else { 127 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativePretimeoutInterruptDesc, prometheus.GaugeValue, 0, pretimeoutInterrupt) 128 | } 129 | } 130 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativePretimeoutIntervalDesc, prometheus.GaugeValue, float64(res.PreTimeoutIntervalSec)) 131 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativeInitialCountdownDesc, prometheus.GaugeValue, float64(res.InitialCountdown)) 132 | ch <- prometheus.MustNewConstMetric(bmcWatchdogNativeCurrentCountdownDesc, prometheus.GaugeValue, float64(res.PresentCountdown)) 133 | return 1, nil 134 | } 135 | -------------------------------------------------------------------------------- /collector_chassis.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "github.com/prometheus/client_golang/prometheus" 18 | 19 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 20 | ) 21 | 22 | const ( 23 | ChassisCollectorName CollectorName = "chassis" 24 | ) 25 | 26 | var ( 27 | chassisPowerStateDesc = prometheus.NewDesc( 28 | prometheus.BuildFQName(namespace, "chassis", "power_state"), 29 | "Current power state (1=on, 0=off).", 30 | []string{}, 31 | nil, 32 | ) 33 | chassisDriveFaultDesc = prometheus.NewDesc( 34 | prometheus.BuildFQName(namespace, "chassis", "drive_fault_state"), 35 | "Current drive fault state (1=false, 0=true).", 36 | []string{}, 37 | nil, 38 | ) 39 | chassisCoolingFaultDesc = prometheus.NewDesc( 40 | prometheus.BuildFQName(namespace, "chassis", "cooling_fault_state"), 41 | "Current Cooling/fan fault state (1=false, 0=true).", 42 | []string{}, 43 | nil, 44 | ) 45 | ) 46 | 47 | type ChassisCollector struct{} 48 | 49 | func (c ChassisCollector) Name() CollectorName { 50 | return ChassisCollectorName 51 | } 52 | 53 | func (c ChassisCollector) Cmd() string { 54 | return "ipmi-chassis" 55 | } 56 | 57 | func (c ChassisCollector) Args() []string { 58 | return []string{"--get-chassis-status"} 59 | } 60 | 61 | func (c ChassisCollector) Collect(result freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 62 | currentChassisPowerState, err := freeipmi.GetChassisPowerState(result) 63 | if err != nil { 64 | logger.Error("Failed to collect chassis data", "target", targetName(target.host), "error", err) 65 | return 0, err 66 | } 67 | currentChassisDriveFault, err := freeipmi.GetChassisDriveFault(result) 68 | if err != nil { 69 | logger.Error("Failed to collect chassis data", "target", targetName(target.host), "error", err) 70 | return 0, err 71 | } 72 | currentChassisCoolingFault, err := freeipmi.GetChassisCoolingFault(result) 73 | if err != nil { 74 | logger.Error("Failed to collect chassis data", "target", targetName(target.host), "error", err) 75 | return 0, err 76 | } 77 | ch <- prometheus.MustNewConstMetric( 78 | chassisPowerStateDesc, 79 | prometheus.GaugeValue, 80 | currentChassisPowerState, 81 | ) 82 | ch <- prometheus.MustNewConstMetric( 83 | chassisDriveFaultDesc, 84 | prometheus.GaugeValue, 85 | currentChassisDriveFault, 86 | ) 87 | ch <- prometheus.MustNewConstMetric( 88 | chassisCoolingFaultDesc, 89 | prometheus.GaugeValue, 90 | currentChassisCoolingFault, 91 | ) 92 | return 1, nil 93 | } 94 | -------------------------------------------------------------------------------- /collector_chassis_native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | 21 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 22 | ) 23 | 24 | var ( 25 | chassisNativePowerStateDesc = prometheus.NewDesc( 26 | prometheus.BuildFQName(namespace, "chassis", "power_state"), 27 | "Current power state (1=on, 0=off).", 28 | []string{}, 29 | nil, 30 | ) 31 | chassisNativeDriveFaultDesc = prometheus.NewDesc( 32 | prometheus.BuildFQName(namespace, "chassis", "drive_fault_state"), 33 | "Current drive fault state (1=true, 0=false).", // TODO value mapping changed 34 | []string{}, 35 | nil, 36 | ) 37 | chassisNativeCoolingFaultDesc = prometheus.NewDesc( 38 | prometheus.BuildFQName(namespace, "chassis", "cooling_fault_state"), 39 | "Current Cooling/fan fault state (1=true, 0=false).", // TODO value mapping changed 40 | []string{}, 41 | nil, 42 | ) 43 | ) 44 | 45 | type ChassisNativeCollector struct{} 46 | 47 | func (c ChassisNativeCollector) Name() CollectorName { 48 | // The name is intentionally the same as the non-native collector 49 | return ChassisCollectorName 50 | } 51 | 52 | func (c ChassisNativeCollector) Cmd() string { 53 | return "" 54 | } 55 | 56 | func (c ChassisNativeCollector) Args() []string { 57 | return []string{} 58 | } 59 | 60 | func (c ChassisNativeCollector) Collect(_ freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 61 | client, err := NewNativeClient(target) 62 | if err != nil { 63 | return 0, err 64 | } 65 | res, err := client.GetChassisStatus(context.TODO()) 66 | if err != nil { 67 | return 0, err 68 | } 69 | 70 | ch <- prometheus.MustNewConstMetric( 71 | chassisNativePowerStateDesc, 72 | prometheus.GaugeValue, 73 | boolToFloat(res.PowerIsOn), 74 | ) 75 | ch <- prometheus.MustNewConstMetric( 76 | chassisNativeDriveFaultDesc, 77 | prometheus.GaugeValue, 78 | boolToFloat(res.DriveFault), 79 | ) 80 | ch <- prometheus.MustNewConstMetric( 81 | chassisNativeCoolingFaultDesc, 82 | prometheus.GaugeValue, 83 | boolToFloat(res.CollingFanFault), 84 | ) 85 | return 1, nil 86 | } 87 | 88 | func boolToFloat(b bool) float64 { 89 | if b { 90 | return 1.0 91 | } 92 | return 0.0 93 | } 94 | -------------------------------------------------------------------------------- /collector_dcmi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "github.com/prometheus/client_golang/prometheus" 18 | 19 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 20 | ) 21 | 22 | const ( 23 | DCMICollectorName CollectorName = "dcmi" 24 | ) 25 | 26 | var ( 27 | powerConsumptionDesc = prometheus.NewDesc( 28 | prometheus.BuildFQName(namespace, "dcmi", "power_consumption_watts"), 29 | "Current power consumption in Watts.", 30 | []string{}, 31 | nil, 32 | ) 33 | ) 34 | 35 | type DCMICollector struct{} 36 | 37 | func (c DCMICollector) Name() CollectorName { 38 | return DCMICollectorName 39 | } 40 | 41 | func (c DCMICollector) Cmd() string { 42 | return "ipmi-dcmi" 43 | } 44 | 45 | func (c DCMICollector) Args() []string { 46 | return []string{"--get-system-power-statistics"} 47 | } 48 | 49 | func (c DCMICollector) Collect(result freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 50 | currentPowerConsumption, err := freeipmi.GetCurrentPowerConsumption(result) 51 | if err != nil { 52 | logger.Error("Failed to collect DCMI data", "target", targetName(target.host), "error", err) 53 | return 0, err 54 | } 55 | // Returned value negative == Power Measurement is not avail 56 | if currentPowerConsumption > -1 { 57 | ch <- prometheus.MustNewConstMetric( 58 | powerConsumptionDesc, 59 | prometheus.GaugeValue, 60 | currentPowerConsumption, 61 | ) 62 | } 63 | return 1, nil 64 | } 65 | -------------------------------------------------------------------------------- /collector_dcmi_native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | 21 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 22 | ) 23 | 24 | var ( 25 | powerConsumptionNativeDesc = prometheus.NewDesc( 26 | prometheus.BuildFQName(namespace, "dcmi", "power_consumption_watts"), 27 | "Current power consumption in Watts.", 28 | []string{}, 29 | nil, 30 | ) 31 | ) 32 | 33 | type DCMINativeCollector struct{} 34 | 35 | func (c DCMINativeCollector) Name() CollectorName { 36 | // The name is intentionally the same as the non-native collector 37 | return DCMICollectorName 38 | } 39 | 40 | func (c DCMINativeCollector) Cmd() string { 41 | return "" 42 | } 43 | 44 | func (c DCMINativeCollector) Args() []string { 45 | return []string{} 46 | } 47 | 48 | func (c DCMINativeCollector) Collect(_ freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 49 | client, err := NewNativeClient(target) 50 | if err != nil { 51 | return 0, err 52 | } 53 | res, err := client.GetDCMIPowerReading(context.TODO()) 54 | if err != nil { 55 | logger.Error("Failed to collect DCMI data", "target", targetName(target.host), "error", err) 56 | return 0, err 57 | } 58 | 59 | if res.PowerMeasurementActive { 60 | ch <- prometheus.MustNewConstMetric( 61 | powerConsumptionNativeDesc, 62 | prometheus.GaugeValue, 63 | float64(res.CurrentPower), 64 | ) 65 | } 66 | return 1, nil 67 | } 68 | -------------------------------------------------------------------------------- /collector_ipmi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "math" 19 | "strconv" 20 | 21 | "github.com/prometheus/client_golang/prometheus" 22 | 23 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 24 | ) 25 | 26 | const ( 27 | IPMICollectorName CollectorName = "ipmi" 28 | ) 29 | 30 | var ( 31 | sensorStateDesc = prometheus.NewDesc( 32 | prometheus.BuildFQName(namespace, "sensor", "state"), 33 | "Indicates the severity of the state reported by an IPMI sensor (0=nominal, 1=warning, 2=critical).", 34 | []string{"id", "name", "type"}, 35 | nil, 36 | ) 37 | 38 | sensorValueDesc = prometheus.NewDesc( 39 | prometheus.BuildFQName(namespace, "sensor", "value"), 40 | "Generic data read from an IPMI sensor of unknown type, relying on labels for context.", 41 | []string{"id", "name", "type"}, 42 | nil, 43 | ) 44 | 45 | fanSpeedRPMDesc = prometheus.NewDesc( 46 | prometheus.BuildFQName(namespace, "fan_speed", "rpm"), 47 | "Fan speed in rotations per minute.", 48 | []string{"id", "name"}, 49 | nil, 50 | ) 51 | 52 | fanSpeedRatioDesc = prometheus.NewDesc( 53 | prometheus.BuildFQName(namespace, "fan_speed", "ratio"), 54 | "Fan speed as a proportion of the maximum speed.", 55 | []string{"id", "name"}, 56 | nil, 57 | ) 58 | 59 | fanSpeedStateDesc = prometheus.NewDesc( 60 | prometheus.BuildFQName(namespace, "fan_speed", "state"), 61 | "Reported state of a fan speed sensor (0=nominal, 1=warning, 2=critical).", 62 | []string{"id", "name"}, 63 | nil, 64 | ) 65 | 66 | temperatureDesc = prometheus.NewDesc( 67 | prometheus.BuildFQName(namespace, "temperature", "celsius"), 68 | "Temperature reading in degree Celsius.", 69 | []string{"id", "name"}, 70 | nil, 71 | ) 72 | 73 | temperatureStateDesc = prometheus.NewDesc( 74 | prometheus.BuildFQName(namespace, "temperature", "state"), 75 | "Reported state of a temperature sensor (0=nominal, 1=warning, 2=critical).", 76 | []string{"id", "name"}, 77 | nil, 78 | ) 79 | 80 | voltageDesc = prometheus.NewDesc( 81 | prometheus.BuildFQName(namespace, "voltage", "volts"), 82 | "Voltage reading in Volts.", 83 | []string{"id", "name"}, 84 | nil, 85 | ) 86 | 87 | voltageStateDesc = prometheus.NewDesc( 88 | prometheus.BuildFQName(namespace, "voltage", "state"), 89 | "Reported state of a voltage sensor (0=nominal, 1=warning, 2=critical).", 90 | []string{"id", "name"}, 91 | nil, 92 | ) 93 | 94 | currentDesc = prometheus.NewDesc( 95 | prometheus.BuildFQName(namespace, "current", "amperes"), 96 | "Current reading in Amperes.", 97 | []string{"id", "name"}, 98 | nil, 99 | ) 100 | 101 | currentStateDesc = prometheus.NewDesc( 102 | prometheus.BuildFQName(namespace, "current", "state"), 103 | "Reported state of a current sensor (0=nominal, 1=warning, 2=critical).", 104 | []string{"id", "name"}, 105 | nil, 106 | ) 107 | 108 | powerDesc = prometheus.NewDesc( 109 | prometheus.BuildFQName(namespace, "power", "watts"), 110 | "Power reading in Watts.", 111 | []string{"id", "name"}, 112 | nil, 113 | ) 114 | 115 | powerStateDesc = prometheus.NewDesc( 116 | prometheus.BuildFQName(namespace, "power", "state"), 117 | "Reported state of a power sensor (0=nominal, 1=warning, 2=critical).", 118 | []string{"id", "name"}, 119 | nil, 120 | ) 121 | ) 122 | 123 | type IPMICollector struct{} 124 | 125 | func (c IPMICollector) Name() CollectorName { 126 | return IPMICollectorName 127 | } 128 | 129 | func (c IPMICollector) Cmd() string { 130 | return "ipmimonitoring" 131 | } 132 | 133 | func (c IPMICollector) Args() []string { 134 | return []string{ 135 | "--quiet-cache", 136 | "--ignore-unrecognized-events", 137 | "--comma-separated-output", 138 | "--no-header-output", 139 | "--sdr-cache-recreate", 140 | "--output-event-bitmask", 141 | "--output-sensor-state", 142 | } 143 | } 144 | 145 | func (c IPMICollector) Collect(result freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 146 | excludeIDs := target.config.ExcludeSensorIDs 147 | targetHost := targetName(target.host) 148 | results, err := freeipmi.GetSensorData(result, excludeIDs) 149 | if err != nil { 150 | logger.Error("Failed to collect sensor data", "target", targetHost, "error", err) 151 | return 0, err 152 | } 153 | for _, data := range results { 154 | var state float64 155 | 156 | switch data.State { 157 | case "Nominal": 158 | state = 0 159 | case "Warning": 160 | state = 1 161 | case "Critical": 162 | state = 2 163 | case "N/A": 164 | state = math.NaN() 165 | default: 166 | logger.Error("Unknown sensor state", "target", targetHost, "state", data.State) 167 | state = math.NaN() 168 | } 169 | 170 | logger.Debug("Got values", "target", targetHost, "data", fmt.Sprintf("%+v", data)) 171 | 172 | switch data.Unit { 173 | case "RPM": 174 | collectTypedSensor(ch, fanSpeedRPMDesc, fanSpeedStateDesc, state, data, 1.0) 175 | case "C": 176 | collectTypedSensor(ch, temperatureDesc, temperatureStateDesc, state, data, 1.0) 177 | case "A": 178 | collectTypedSensor(ch, currentDesc, currentStateDesc, state, data, 1.0) 179 | case "V": 180 | collectTypedSensor(ch, voltageDesc, voltageStateDesc, state, data, 1.0) 181 | case "W": 182 | collectTypedSensor(ch, powerDesc, powerStateDesc, state, data, 1.0) 183 | case "%": 184 | switch data.Type { 185 | case "Fan": 186 | collectTypedSensor(ch, fanSpeedRatioDesc, fanSpeedStateDesc, state, data, 0.01) 187 | default: 188 | collectGenericSensor(ch, state, data) 189 | } 190 | default: 191 | collectGenericSensor(ch, state, data) 192 | } 193 | } 194 | return 1, nil 195 | } 196 | 197 | func (c IPMICollector) Describe(ch chan<- *prometheus.Desc) { 198 | ch <- sensorStateDesc 199 | ch <- sensorValueDesc 200 | ch <- fanSpeedRPMDesc 201 | ch <- fanSpeedRatioDesc 202 | ch <- fanSpeedStateDesc 203 | ch <- temperatureDesc 204 | ch <- temperatureStateDesc 205 | ch <- voltageDesc 206 | ch <- voltageStateDesc 207 | ch <- currentDesc 208 | ch <- currentStateDesc 209 | ch <- powerDesc 210 | ch <- powerStateDesc 211 | } 212 | 213 | func collectTypedSensor(ch chan<- prometheus.Metric, desc, stateDesc *prometheus.Desc, state float64, data freeipmi.SensorData, scale float64) { 214 | ch <- prometheus.MustNewConstMetric( 215 | desc, 216 | prometheus.GaugeValue, 217 | data.Value*scale, 218 | strconv.FormatInt(data.ID, 10), 219 | data.Name, 220 | ) 221 | ch <- prometheus.MustNewConstMetric( 222 | stateDesc, 223 | prometheus.GaugeValue, 224 | state, 225 | strconv.FormatInt(data.ID, 10), 226 | data.Name, 227 | ) 228 | } 229 | 230 | func collectGenericSensor(ch chan<- prometheus.Metric, state float64, data freeipmi.SensorData) { 231 | ch <- prometheus.MustNewConstMetric( 232 | sensorValueDesc, 233 | prometheus.GaugeValue, 234 | data.Value, 235 | strconv.FormatInt(data.ID, 10), 236 | data.Name, 237 | data.Type, 238 | ) 239 | ch <- prometheus.MustNewConstMetric( 240 | sensorStateDesc, 241 | prometheus.GaugeValue, 242 | state, 243 | strconv.FormatInt(data.ID, 10), 244 | data.Name, 245 | data.Type, 246 | ) 247 | } 248 | -------------------------------------------------------------------------------- /collector_ipmi_native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "math" 20 | "strconv" 21 | 22 | "github.com/bougou/go-ipmi" 23 | "github.com/prometheus/client_golang/prometheus" 24 | 25 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 26 | ) 27 | 28 | var ( 29 | sensorStateNativeDesc = prometheus.NewDesc( 30 | prometheus.BuildFQName(namespace, "sensor", "state"), 31 | "Indicates the severity of the state reported by an IPMI sensor (0=nominal, 1=warning, 2=critical, 3=non-recoverable).", 32 | []string{"id", "name", "type"}, 33 | nil, 34 | ) 35 | 36 | sensorValueNativeDesc = prometheus.NewDesc( 37 | prometheus.BuildFQName(namespace, "sensor", "value"), 38 | "Generic data read from an IPMI sensor of unknown type, relying on labels for context.", 39 | []string{"id", "name", "type"}, 40 | nil, 41 | ) 42 | 43 | fanSpeedRPMNativeDesc = prometheus.NewDesc( 44 | prometheus.BuildFQName(namespace, "fan_speed", "rpm"), 45 | "Fan speed in rotations per minute.", 46 | []string{"id", "name"}, 47 | nil, 48 | ) 49 | 50 | fanSpeedRatioNativeDesc = prometheus.NewDesc( 51 | prometheus.BuildFQName(namespace, "fan_speed", "ratio"), 52 | "Fan speed as a proportion of the maximum speed.", 53 | []string{"id", "name"}, 54 | nil, 55 | ) 56 | 57 | fanSpeedStateNativeDesc = prometheus.NewDesc( 58 | prometheus.BuildFQName(namespace, "fan_speed", "state"), 59 | "Reported state of a fan speed sensor (0=nominal, 1=warning, 2=critical, 3=non-recoverable).", 60 | []string{"id", "name"}, 61 | nil, 62 | ) 63 | 64 | temperatureNativeDesc = prometheus.NewDesc( 65 | prometheus.BuildFQName(namespace, "temperature", "celsius"), 66 | "Temperature reading in degree Celsius.", 67 | []string{"id", "name"}, 68 | nil, 69 | ) 70 | 71 | temperatureStateNativeDesc = prometheus.NewDesc( 72 | prometheus.BuildFQName(namespace, "temperature", "state"), 73 | "Reported state of a temperature sensor (0=nominal, 1=warning, 2=critical, 3=non-recoverable).", 74 | []string{"id", "name"}, 75 | nil, 76 | ) 77 | 78 | voltageNativeDesc = prometheus.NewDesc( 79 | prometheus.BuildFQName(namespace, "voltage", "volts"), 80 | "Voltage reading in Volts.", 81 | []string{"id", "name"}, 82 | nil, 83 | ) 84 | 85 | voltageStateNativeDesc = prometheus.NewDesc( 86 | prometheus.BuildFQName(namespace, "voltage", "state"), 87 | "Reported state of a voltage sensor (0=nominal, 1=warning, 2=critical, 3=non-recoverable).", 88 | []string{"id", "name"}, 89 | nil, 90 | ) 91 | 92 | currentNativeDesc = prometheus.NewDesc( 93 | prometheus.BuildFQName(namespace, "current", "amperes"), 94 | "Current reading in Amperes.", 95 | []string{"id", "name"}, 96 | nil, 97 | ) 98 | 99 | currentStateNativeDesc = prometheus.NewDesc( 100 | prometheus.BuildFQName(namespace, "current", "state"), 101 | "Reported state of a current sensor (0=nominal, 1=warning, 2=critical, 3=non-recoverable).", 102 | []string{"id", "name"}, 103 | nil, 104 | ) 105 | 106 | powerNativeDesc = prometheus.NewDesc( 107 | prometheus.BuildFQName(namespace, "power", "watts"), 108 | "Power reading in Watts.", 109 | []string{"id", "name"}, 110 | nil, 111 | ) 112 | 113 | powerStateNativeDesc = prometheus.NewDesc( 114 | prometheus.BuildFQName(namespace, "power", "state"), 115 | "Reported state of a power sensor (0=nominal, 1=warning, 2=critical, 3=non-recoverable).", 116 | []string{"id", "name"}, 117 | nil, 118 | ) 119 | ) 120 | 121 | type IPMINativeCollector struct{} 122 | 123 | func (c IPMINativeCollector) Name() CollectorName { 124 | // The name is intentionally the same as the non-native collector 125 | return IPMICollectorName 126 | } 127 | 128 | func (c IPMINativeCollector) Cmd() string { 129 | return "" 130 | } 131 | 132 | func (c IPMINativeCollector) Args() []string { 133 | return []string{} 134 | } 135 | 136 | func (c IPMINativeCollector) Collect(_ freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 137 | excludeIDs := target.config.ExcludeSensorIDs 138 | targetHost := targetName(target.host) 139 | 140 | filter := func(sensor *ipmi.Sensor) bool { 141 | for _, id := range excludeIDs { 142 | if id == int64(sensor.Number) { 143 | return false 144 | } 145 | } 146 | return true 147 | } 148 | 149 | client, err := NewNativeClient(target) 150 | if err != nil { 151 | return 0, err 152 | } 153 | res, err := client.GetSensors(context.TODO(), filter) 154 | if err != nil { 155 | return 0, err 156 | } 157 | 158 | // results, err := freeipmi.GetSensorData(result, excludeIds) 159 | // if err != nil { 160 | // logger.Error("Failed to collect sensor data", "target", targetHost, "error", err) 161 | // return 0, err 162 | // } 163 | for _, data := range res { 164 | var state float64 165 | 166 | switch data.Status() { 167 | case "ok": 168 | state = 0 169 | case "lnc", "unc": // lower/upper non-critical 170 | state = 1 171 | case "lcr", "ucr": // lower/upper critical 172 | state = 2 173 | case "lnr", "unr": // lower/upper non-recoverable 174 | state = 3 // TODO this is new 175 | case "N/A": 176 | state = math.NaN() 177 | default: 178 | logger.Error("Unknown sensor state", "target", targetHost, "state", data.Status()) 179 | state = math.NaN() 180 | } 181 | 182 | logger.Debug("Got values", "target", targetHost, "data", fmt.Sprintf("%+v", data)) 183 | 184 | // TODO this could be greatly improved, now that we have structured data available 185 | switch data.SensorUnit.BaseUnit { 186 | case ipmi.SensorUnitType_RPM: 187 | if data.SensorUnit.Percentage { 188 | collectTypedSensorNative(ch, fanSpeedRatioNativeDesc, fanSpeedStateNativeDesc, state, data, 0.01) 189 | } else { 190 | 191 | collectTypedSensorNative(ch, fanSpeedRPMNativeDesc, fanSpeedStateNativeDesc, state, data, 1.0) 192 | } 193 | case ipmi.SensorUnitType_DegreesC: 194 | collectTypedSensorNative(ch, temperatureNativeDesc, temperatureStateNativeDesc, state, data, 1.0) 195 | case ipmi.SensorUnitType_Amps: 196 | collectTypedSensorNative(ch, currentNativeDesc, currentStateNativeDesc, state, data, 1.0) 197 | case ipmi.SensorUnitType_Volts: 198 | collectTypedSensorNative(ch, voltageNativeDesc, voltageStateNativeDesc, state, data, 1.0) 199 | case ipmi.SensorUnitType_Watts: 200 | collectTypedSensorNative(ch, powerNativeDesc, powerStateNativeDesc, state, data, 1.0) 201 | default: 202 | collectGenericSensorNative(ch, state, data) 203 | } 204 | } 205 | return 1, nil 206 | } 207 | 208 | func (c IPMINativeCollector) Describe(ch chan<- *prometheus.Desc) { 209 | ch <- sensorStateDesc 210 | ch <- sensorValueDesc 211 | ch <- fanSpeedRPMDesc 212 | ch <- fanSpeedRatioDesc 213 | ch <- fanSpeedStateDesc 214 | ch <- temperatureDesc 215 | ch <- temperatureStateDesc 216 | ch <- voltageDesc 217 | ch <- voltageStateDesc 218 | ch <- currentDesc 219 | ch <- currentStateDesc 220 | ch <- powerDesc 221 | ch <- powerStateDesc 222 | } 223 | 224 | func collectTypedSensorNative(ch chan<- prometheus.Metric, desc, stateDesc *prometheus.Desc, state float64, data *ipmi.Sensor, scale float64) { 225 | ch <- prometheus.MustNewConstMetric( 226 | desc, 227 | prometheus.GaugeValue, 228 | data.Value*scale, 229 | strconv.FormatInt(int64(data.Number), 10), 230 | data.Name, 231 | ) 232 | ch <- prometheus.MustNewConstMetric( 233 | stateDesc, 234 | prometheus.GaugeValue, 235 | state, 236 | strconv.FormatInt(int64(data.Number), 10), 237 | data.Name, 238 | ) 239 | } 240 | 241 | func collectGenericSensorNative(ch chan<- prometheus.Metric, state float64, data *ipmi.Sensor) { 242 | ch <- prometheus.MustNewConstMetric( 243 | sensorValueNativeDesc, 244 | prometheus.GaugeValue, 245 | data.Value, 246 | strconv.FormatInt(int64(data.Number), 10), 247 | data.Name, 248 | data.SensorType.String(), 249 | ) 250 | ch <- prometheus.MustNewConstMetric( 251 | sensorStateNativeDesc, 252 | prometheus.GaugeValue, 253 | state, 254 | strconv.FormatInt(int64(data.Number), 10), 255 | data.Name, 256 | data.SensorType.String(), 257 | ) 258 | } 259 | -------------------------------------------------------------------------------- /collector_sel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "github.com/prometheus/client_golang/prometheus" 18 | 19 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 20 | ) 21 | 22 | const ( 23 | SELCollectorName CollectorName = "sel" 24 | ) 25 | 26 | var ( 27 | selEntriesCountDesc = prometheus.NewDesc( 28 | prometheus.BuildFQName(namespace, "sel", "logs_count"), 29 | "Current number of log entries in the SEL.", 30 | []string{}, 31 | nil, 32 | ) 33 | 34 | selFreeSpaceDesc = prometheus.NewDesc( 35 | prometheus.BuildFQName(namespace, "sel", "free_space_bytes"), 36 | "Current free space remaining for new SEL entries.", 37 | []string{}, 38 | nil, 39 | ) 40 | ) 41 | 42 | type SELCollector struct{} 43 | 44 | func (c SELCollector) Name() CollectorName { 45 | return SELCollectorName 46 | } 47 | 48 | func (c SELCollector) Cmd() string { 49 | return "ipmi-sel" 50 | } 51 | 52 | func (c SELCollector) Args() []string { 53 | return []string{"--info"} 54 | } 55 | 56 | func (c SELCollector) Collect(result freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 57 | entriesCount, err := freeipmi.GetSELInfoEntriesCount(result) 58 | if err != nil { 59 | logger.Error("Failed to collect SEL data", "target", targetName(target.host), "error", err) 60 | return 0, err 61 | } 62 | freeSpace, err := freeipmi.GetSELInfoFreeSpace(result) 63 | if err != nil { 64 | logger.Error("Failed to collect SEL data", "target", targetName(target.host), "error", err) 65 | return 0, err 66 | } 67 | ch <- prometheus.MustNewConstMetric( 68 | selEntriesCountDesc, 69 | prometheus.GaugeValue, 70 | entriesCount, 71 | ) 72 | ch <- prometheus.MustNewConstMetric( 73 | selFreeSpaceDesc, 74 | prometheus.GaugeValue, 75 | freeSpace, 76 | ) 77 | return 1, nil 78 | } 79 | -------------------------------------------------------------------------------- /collector_sel_events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "time" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | 21 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 22 | ) 23 | 24 | const ( 25 | SELEventsCollectorName CollectorName = "sel-events" 26 | SELDateTimeFormat string = "Jan-02-2006 15:04:05" 27 | ) 28 | 29 | var ( 30 | selEventsCountByStateDesc = prometheus.NewDesc( 31 | prometheus.BuildFQName(namespace, "sel_events", "count_by_state"), 32 | "Current number of log entries in the SEL by state.", 33 | []string{"state"}, 34 | nil, 35 | ) 36 | selEventsCountByNameDesc = prometheus.NewDesc( 37 | prometheus.BuildFQName(namespace, "sel_events", "count_by_name"), 38 | "Current number of custom log entries in the SEL by name.", 39 | []string{"name"}, 40 | nil, 41 | ) 42 | selEventsLatestTimestampDesc = prometheus.NewDesc( 43 | prometheus.BuildFQName(namespace, "sel_events", "latest_timestamp"), 44 | "Latest timestamp of custom log entries in the SEL by name.", 45 | []string{"name"}, 46 | nil, 47 | ) 48 | ) 49 | 50 | type SELEventsCollector struct{} 51 | 52 | func (c SELEventsCollector) Name() CollectorName { 53 | return SELEventsCollectorName 54 | } 55 | 56 | func (c SELEventsCollector) Cmd() string { 57 | return "ipmi-sel" 58 | } 59 | 60 | func (c SELEventsCollector) Args() []string { 61 | return []string{ 62 | "--quiet-cache", 63 | "--comma-separated-output", 64 | "--no-header-output", 65 | "--sdr-cache-recreate", 66 | "--output-event-state", 67 | "--interpret-oem-data", 68 | "--entity-sensor-names", 69 | } 70 | } 71 | 72 | func (c SELEventsCollector) Collect(result freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 73 | selEventConfigs := target.config.SELEvents 74 | 75 | events, err := freeipmi.GetSELEvents(result) 76 | if err != nil { 77 | logger.Error("Failed to collect SEL events", "target", targetName(target.host), "error", err) 78 | return 0, err 79 | } 80 | 81 | selEventByStateCount := map[string]float64{} 82 | selEventByNameCount := map[string]float64{} 83 | selEventByNameTimestamp := map[string]float64{} 84 | 85 | // initialize sel event metrics by zero 86 | for _, metricConfig := range selEventConfigs { 87 | selEventByNameTimestamp[metricConfig.Name] = 0 88 | selEventByNameCount[metricConfig.Name] = 0 89 | } 90 | 91 | var newTimestamp float64 92 | for _, data := range events { 93 | for _, metricConfig := range selEventConfigs { 94 | match := metricConfig.Regex.FindStringSubmatch(data.Event) 95 | if match != nil { 96 | newTimestamp = 0.0 97 | datetime := data.Date + " " + data.Time 98 | t, err := time.Parse(SELDateTimeFormat, datetime) 99 | // ignore errors with invalid date or time 100 | // NOTE: in some cases ipmi-sel can return "PostInit" in Date and Time fields 101 | // Example: 102 | // $ ipmi-sel --comma-separated-output --output-event-state --interpret-oem-data --output-oem-event-strings 103 | // ID,Date,Time,Name,Type,State,Event 104 | // 3,PostInit,PostInit,Sensor #211,Memory,Warning,Correctable memory error ; Event Data3 = 34h 105 | if err != nil { 106 | logger.Debug("Failed to parse time", "target", targetName(target.host), "error", err) 107 | } else { 108 | newTimestamp = float64(t.Unix()) 109 | } 110 | // save latest timestamp by name metrics 111 | if newTimestamp > selEventByNameTimestamp[metricConfig.Name] { 112 | selEventByNameTimestamp[metricConfig.Name] = newTimestamp 113 | } 114 | // save count by name metrics 115 | selEventByNameCount[metricConfig.Name]++ 116 | } 117 | } 118 | // save count by state metrics 119 | _, ok := selEventByStateCount[data.State] 120 | if !ok { 121 | selEventByStateCount[data.State] = 0 122 | } 123 | selEventByStateCount[data.State]++ 124 | } 125 | 126 | for state, value := range selEventByStateCount { 127 | ch <- prometheus.MustNewConstMetric( 128 | selEventsCountByStateDesc, 129 | prometheus.GaugeValue, 130 | value, 131 | state, 132 | ) 133 | } 134 | 135 | for name, value := range selEventByNameCount { 136 | ch <- prometheus.MustNewConstMetric( 137 | selEventsCountByNameDesc, 138 | prometheus.GaugeValue, 139 | value, 140 | name, 141 | ) 142 | ch <- prometheus.MustNewConstMetric( 143 | selEventsLatestTimestampDesc, 144 | prometheus.GaugeValue, 145 | selEventByNameTimestamp[name], 146 | name, 147 | ) 148 | } 149 | return 1, nil 150 | } 151 | -------------------------------------------------------------------------------- /collector_sel_events_native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | 21 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 22 | ) 23 | 24 | var ( 25 | selEventsCountByStateNativeDesc = prometheus.NewDesc( 26 | prometheus.BuildFQName(namespace, "sel_events", "count_by_state"), 27 | "Current number of log entries in the SEL by state.", 28 | []string{"state"}, 29 | nil, 30 | ) 31 | selEventsCountByNameNativeDesc = prometheus.NewDesc( 32 | prometheus.BuildFQName(namespace, "sel_events", "count_by_name"), 33 | "Current number of custom log entries in the SEL by name.", 34 | []string{"name"}, 35 | nil, 36 | ) 37 | selEventsLatestTimestampNativeDesc = prometheus.NewDesc( 38 | prometheus.BuildFQName(namespace, "sel_events", "latest_timestamp"), 39 | "Latest timestamp of custom log entries in the SEL by name.", 40 | []string{"name"}, 41 | nil, 42 | ) 43 | ) 44 | 45 | type SELEventsNativeCollector struct{} 46 | 47 | func (c SELEventsNativeCollector) Name() CollectorName { 48 | // The name is intentionally the same as the non-native collector 49 | return SELEventsCollectorName 50 | } 51 | 52 | func (c SELEventsNativeCollector) Cmd() string { 53 | return "" 54 | } 55 | 56 | func (c SELEventsNativeCollector) Args() []string { 57 | return []string{} 58 | } 59 | 60 | func (c SELEventsNativeCollector) Collect(_ freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 61 | selEventConfigs := target.config.SELEvents 62 | 63 | client, err := NewNativeClient(target) 64 | if err != nil { 65 | return 0, err 66 | } 67 | res, err := client.GetSELEntries(context.TODO(), 0) 68 | if err != nil { 69 | return 0, err 70 | } 71 | 72 | selEventByStateCount := map[string]float64{} 73 | selEventByNameCount := map[string]float64{} 74 | selEventByNameTimestamp := map[string]float64{} 75 | 76 | // initialize sel event metrics by zero 77 | for _, metricConfig := range selEventConfigs { 78 | selEventByNameTimestamp[metricConfig.Name] = 0 79 | selEventByNameCount[metricConfig.Name] = 0 80 | } 81 | 82 | for _, data := range res { 83 | for _, metricConfig := range selEventConfigs { 84 | match := metricConfig.Regex.FindStringSubmatch(data.Standard.EventString()) 85 | logger.Debug("event regex", "regex", metricConfig.RegexRaw, "input", data.Standard.EventString(), "match", match) 86 | if match != nil { 87 | var newTimestamp = float64(data.Standard.Timestamp.Unix()) 88 | // datetime := data.Date + " " + data.Time 89 | // t, err := time.Parse(SELDateTimeFormat, datetime) 90 | // ignore errors with invalid date or time 91 | // NOTE: in some cases ipmi-sel can return "PostInit" in Date and Time fields 92 | // Example: 93 | // $ ipmi-sel --comma-separated-output --output-event-state --interpret-oem-data --output-oem-event-strings 94 | // ID,Date,Time,Name,Type,State,Event 95 | // 3,PostInit,PostInit,Sensor #211,Memory,Warning,Correctable memory error ; Event Data3 = 34h 96 | // if err != nil { 97 | // logger.Debug("Failed to parse time", "target", targetName(target.host), "error", err) 98 | // } else { 99 | // newTimestamp = float64(t.Unix()) 100 | // } 101 | // save latest timestamp by name metrics 102 | if newTimestamp > selEventByNameTimestamp[metricConfig.Name] { 103 | selEventByNameTimestamp[metricConfig.Name] = newTimestamp 104 | } 105 | // save count by name metrics 106 | selEventByNameCount[metricConfig.Name]++ 107 | } 108 | } 109 | // save count by state metrics 110 | state := string(data.Standard.EventSeverity()) 111 | _, ok := selEventByStateCount[state] 112 | if !ok { 113 | selEventByStateCount[state] = 0 114 | } 115 | selEventByStateCount[state]++ 116 | } 117 | 118 | for state, value := range selEventByStateCount { 119 | ch <- prometheus.MustNewConstMetric( 120 | selEventsCountByStateNativeDesc, 121 | prometheus.GaugeValue, 122 | value, 123 | state, 124 | ) 125 | } 126 | 127 | for name, value := range selEventByNameCount { 128 | ch <- prometheus.MustNewConstMetric( 129 | selEventsCountByNameNativeDesc, 130 | prometheus.GaugeValue, 131 | value, 132 | name, 133 | ) 134 | ch <- prometheus.MustNewConstMetric( 135 | selEventsLatestTimestampNativeDesc, 136 | prometheus.GaugeValue, 137 | selEventByNameTimestamp[name], 138 | name, 139 | ) 140 | } 141 | return 1, nil 142 | } 143 | -------------------------------------------------------------------------------- /collector_sel_native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | 21 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 22 | ) 23 | 24 | var ( 25 | selEntriesCountNativeDesc = prometheus.NewDesc( 26 | prometheus.BuildFQName(namespace, "sel", "logs_count"), 27 | "Current number of log entries in the SEL.", 28 | []string{}, 29 | nil, 30 | ) 31 | 32 | selFreeSpaceNativeDesc = prometheus.NewDesc( 33 | prometheus.BuildFQName(namespace, "sel", "free_space_bytes"), 34 | "Current free space remaining for new SEL entries.", 35 | []string{}, 36 | nil, 37 | ) 38 | ) 39 | 40 | type SELNativeCollector struct{} 41 | 42 | func (c SELNativeCollector) Name() CollectorName { 43 | // The name is intentionally the same as the non-native collector 44 | return SELCollectorName 45 | } 46 | 47 | func (c SELNativeCollector) Cmd() string { 48 | return "" 49 | } 50 | 51 | func (c SELNativeCollector) Args() []string { 52 | return []string{""} 53 | } 54 | 55 | func (c SELNativeCollector) Collect(_ freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 56 | client, err := NewNativeClient(target) 57 | if err != nil { 58 | return 0, err 59 | } 60 | res, err := client.GetSELInfo(context.TODO()) 61 | if err != nil { 62 | return 0, err 63 | } 64 | 65 | ch <- prometheus.MustNewConstMetric( 66 | selEntriesCountNativeDesc, 67 | prometheus.GaugeValue, 68 | float64(res.Entries), 69 | ) 70 | ch <- prometheus.MustNewConstMetric( 71 | selFreeSpaceNativeDesc, 72 | prometheus.GaugeValue, 73 | float64(res.FreeBytes), 74 | ) 75 | return 1, nil 76 | } 77 | -------------------------------------------------------------------------------- /collector_sm_lan_mode.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "strconv" 19 | 20 | "github.com/prometheus/client_golang/prometheus" 21 | 22 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 23 | ) 24 | 25 | const ( 26 | SMLANModeCollectorName CollectorName = "sm-lan-mode" 27 | ) 28 | 29 | var ( 30 | lanModeDesc = prometheus.NewDesc( 31 | prometheus.BuildFQName(namespace, "config", "lan_mode"), 32 | "Returns configured LAN mode (0=dedicated, 1=shared, 2=failover).", 33 | nil, 34 | nil, 35 | ) 36 | ) 37 | 38 | type SMLANModeCollector struct{} 39 | 40 | func (c SMLANModeCollector) Name() CollectorName { 41 | return SMLANModeCollectorName 42 | } 43 | 44 | func (c SMLANModeCollector) Cmd() string { 45 | return "ipmi-raw" 46 | } 47 | 48 | func (c SMLANModeCollector) Args() []string { 49 | return []string{"0x0", "0x30", "0x70", "0x0c", "0"} 50 | } 51 | 52 | func (c SMLANModeCollector) Collect(result freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 53 | octets, err := freeipmi.GetRawOctets(result) 54 | if err != nil { 55 | logger.Error("Failed to collect LAN mode data", "target", targetName(target.host), "error", err) 56 | return 0, err 57 | } 58 | if len(octets) != 3 { 59 | logger.Error("Unexpected number of octets", "target", targetName(target.host), "octets", octets) 60 | return 0, fmt.Errorf("unexpected number of octets in raw response: %d", len(octets)) 61 | } 62 | 63 | switch octets[2] { 64 | case "00", "01", "02": 65 | value, _ := strconv.Atoi(octets[2]) 66 | ch <- prometheus.MustNewConstMetric(lanModeDesc, prometheus.GaugeValue, float64(value)) 67 | default: 68 | logger.Error("Unexpected lan mode status (ipmi-raw)", "target", targetName(target.host), "sgatus", octets[2]) 69 | return 0, fmt.Errorf("unexpected lan mode status: %s", octets[2]) 70 | } 71 | 72 | return 1, nil 73 | } 74 | -------------------------------------------------------------------------------- /collector_sm_lan_mode_native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | "github.com/bougou/go-ipmi" 21 | "github.com/prometheus/client_golang/prometheus" 22 | 23 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 24 | ) 25 | 26 | var ( 27 | lanModeNativeDesc = prometheus.NewDesc( 28 | prometheus.BuildFQName(namespace, "config", "lan_mode"), 29 | "Returns configured LAN mode (0=dedicated, 1=shared, 2=failover).", 30 | nil, 31 | nil, 32 | ) 33 | ) 34 | 35 | type SMLANModeNativeCollector struct{} 36 | 37 | func (c SMLANModeNativeCollector) Name() CollectorName { 38 | // The name is intentionally the same as the non-native collector 39 | return SMLANModeCollectorName 40 | } 41 | 42 | func (c SMLANModeNativeCollector) Cmd() string { 43 | return "" 44 | } 45 | 46 | func (c SMLANModeNativeCollector) Args() []string { 47 | return []string{} 48 | } 49 | 50 | func (c SMLANModeNativeCollector) Collect(_ freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 51 | client, err := NewNativeClient(target) 52 | if err != nil { 53 | return 0, err 54 | } 55 | if _, err := client.SetSessionPrivilegeLevel(context.TODO(), ipmi.PrivilegeLevelAdministrator); err != nil { 56 | logger.Error("Failed to set privilege level to admin", "target", targetName(target.host)) 57 | return 0, fmt.Errorf("failed to set privilege level to admin") 58 | } 59 | res, err := client.RawCommand(context.TODO(), ipmi.NetFnOEMSupermicroRequest, 0x70, []byte{0x0C, 0x00}, "GetSupermicroLanMode") 60 | if err != nil { 61 | logger.Error("raw command failed", "error", err) 62 | return 0, err 63 | } 64 | 65 | if len(res.Response) != 1 { 66 | logger.Error("Unexpected number of octets", "target", targetName(target.host), "octets", len(res.Response)) 67 | return 0, fmt.Errorf("unexpected number of octets in raw response: %d", len(res.Response)) 68 | } 69 | 70 | value := res.Response[0] 71 | switch value { 72 | case 0, 1, 2: 73 | ch <- prometheus.MustNewConstMetric(lanModeNativeDesc, prometheus.GaugeValue, float64(value)) 74 | default: 75 | logger.Error("Unexpected lan mode status (ipmi-raw)", "target", targetName(target.host), "status", value) 76 | return 0, fmt.Errorf("unexpected lan mode status: %d", value) 77 | } 78 | 79 | return 1, nil 80 | } 81 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "regexp" 20 | "strings" 21 | "sync" 22 | 23 | "github.com/prometheus/client_golang/prometheus" 24 | 25 | "github.com/prometheus-community/ipmi_exporter/freeipmi" 26 | 27 | yaml "gopkg.in/yaml.v2" 28 | ) 29 | 30 | // CollectorName is used for unmarshaling the list of collectors in the yaml config file 31 | type CollectorName string 32 | 33 | // ConfiguredCollector wraps an existing collector implementation, 34 | // potentially altering its default settings. 35 | type ConfiguredCollector struct { 36 | collector collector 37 | command string 38 | defaultArgs []string 39 | customArgs []string 40 | } 41 | 42 | func (c ConfiguredCollector) Name() CollectorName { 43 | return c.collector.Name() 44 | } 45 | 46 | func (c ConfiguredCollector) Cmd() string { 47 | if c.command != "" { 48 | return c.command 49 | } 50 | return c.collector.Cmd() 51 | } 52 | 53 | func (c ConfiguredCollector) Args() []string { 54 | args := []string{} 55 | if c.customArgs != nil { 56 | // custom args come first, this way it is quite easy to 57 | // override a collector to use e.g. sudo 58 | args = append(args, c.customArgs...) 59 | } 60 | if c.defaultArgs != nil { 61 | args = append(args, c.defaultArgs...) 62 | } else { 63 | args = append(args, c.collector.Args()...) 64 | } 65 | return args 66 | } 67 | 68 | func (c ConfiguredCollector) Collect(output freeipmi.Result, ch chan<- prometheus.Metric, target ipmiTarget) (int, error) { 69 | return c.collector.Collect(output, ch, target) 70 | } 71 | 72 | func (c CollectorName) GetInstance() (collector, error) { 73 | // This is where a new collector would have to be "registered" 74 | switch c { 75 | case IPMICollectorName: 76 | if *nativeIPMI { 77 | return IPMINativeCollector{}, nil 78 | } 79 | return IPMICollector{}, nil 80 | case BMCCollectorName: 81 | if *nativeIPMI { 82 | return BMCNativeCollector{}, nil 83 | } 84 | return BMCCollector{}, nil 85 | case BMCWatchdogCollectorName: 86 | if *nativeIPMI { 87 | return BMCWatchdogNativeCollector{}, nil 88 | } 89 | return BMCWatchdogCollector{}, nil 90 | case SELCollectorName: 91 | if *nativeIPMI { 92 | return SELNativeCollector{}, nil 93 | } 94 | return SELCollector{}, nil 95 | case SELEventsCollectorName: 96 | if *nativeIPMI { 97 | return SELEventsNativeCollector{}, nil 98 | } 99 | return SELEventsCollector{}, nil 100 | case DCMICollectorName: 101 | if *nativeIPMI { 102 | return DCMINativeCollector{}, nil 103 | } 104 | return DCMICollector{}, nil 105 | case ChassisCollectorName: 106 | if *nativeIPMI { 107 | return ChassisNativeCollector{}, nil 108 | } 109 | return ChassisCollector{}, nil 110 | case SMLANModeCollectorName: 111 | if *nativeIPMI { 112 | return SMLANModeNativeCollector{}, nil 113 | } 114 | return SMLANModeCollector{}, nil 115 | } 116 | return nil, fmt.Errorf("invalid collector: %s", string(c)) 117 | } 118 | 119 | func (c CollectorName) IsValid() error { 120 | _, err := c.GetInstance() 121 | return err 122 | } 123 | 124 | // Config is the Go representation of the yaml config file. 125 | type Config struct { 126 | Modules map[string]IPMIConfig `yaml:"modules"` 127 | 128 | // Catches all undefined fields and must be empty after parsing. 129 | XXX map[string]interface{} `yaml:",inline"` 130 | } 131 | 132 | // SafeConfig wraps Config for concurrency-safe operations. 133 | type SafeConfig struct { 134 | sync.RWMutex 135 | C *Config 136 | } 137 | 138 | // IPMIConfig is the Go representation of a module configuration in the yaml 139 | // config file. 140 | type IPMIConfig struct { 141 | User string `yaml:"user"` 142 | Password string `yaml:"pass"` 143 | Privilege string `yaml:"privilege"` 144 | Driver string `yaml:"driver"` 145 | Timeout uint32 `yaml:"timeout"` 146 | Collectors []CollectorName `yaml:"collectors"` 147 | ExcludeSensorIDs []int64 `yaml:"exclude_sensor_ids"` 148 | WorkaroundFlags []string `yaml:"workaround_flags"` 149 | CollectorCmd map[CollectorName]string `yaml:"collector_cmd"` 150 | CollectorArgs map[CollectorName][]string `yaml:"default_args"` 151 | CustomArgs map[CollectorName][]string `yaml:"custom_args"` 152 | 153 | SELEvents []*IpmiSELEvent `yaml:"sel_events,omitempty"` 154 | // Catches all undefined fields and must be empty after parsing. 155 | XXX map[string]interface{} `yaml:",inline"` 156 | } 157 | 158 | type IpmiSELEvent struct { 159 | Name string `yaml:"name"` 160 | RegexRaw string `yaml:"regex"` 161 | Regex *regexp.Regexp `yaml:"-"` 162 | } 163 | 164 | var defaultConfig = IPMIConfig{ 165 | Collectors: []CollectorName{IPMICollectorName, DCMICollectorName, BMCCollectorName, ChassisCollectorName}, 166 | } 167 | 168 | func checkOverflow(m map[string]interface{}, ctx string) error { 169 | if len(m) > 0 { 170 | var keys []string 171 | for k := range m { 172 | keys = append(keys, k) 173 | } 174 | return fmt.Errorf("unknown fields in %s: %s", ctx, strings.Join(keys, ", ")) 175 | } 176 | return nil 177 | } 178 | 179 | // UnmarshalYAML implements the yaml.Unmarshaler interface. 180 | func (s *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { 181 | type plain Config 182 | if err := unmarshal((*plain)(s)); err != nil { 183 | return err 184 | } 185 | if err := checkOverflow(s.XXX, "config"); err != nil { 186 | return err 187 | } 188 | return nil 189 | } 190 | 191 | // UnmarshalYAML implements the yaml.Unmarshaler interface. 192 | func (s *IPMIConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 193 | *s = defaultConfig 194 | type plain IPMIConfig 195 | if err := unmarshal((*plain)(s)); err != nil { 196 | return err 197 | } 198 | if err := checkOverflow(s.XXX, "modules"); err != nil { 199 | return err 200 | } 201 | for _, c := range s.Collectors { 202 | if err := c.IsValid(); err != nil { 203 | return err 204 | } 205 | } 206 | for _, selEvent := range s.SELEvents { 207 | selEvent.Regex = regexp.MustCompile(selEvent.RegexRaw) 208 | } 209 | return nil 210 | } 211 | 212 | func (s *IPMIConfig) GetCollectors() []collector { 213 | result := []collector{} 214 | for _, co := range s.Collectors { 215 | // At this point validity has already been checked 216 | i, _ := co.GetInstance() 217 | cc := ConfiguredCollector{ 218 | collector: i, 219 | command: s.CollectorCmd[i.Name()], 220 | defaultArgs: s.CollectorArgs[i.Name()], 221 | customArgs: s.CustomArgs[i.Name()], 222 | } 223 | result = append(result, cc) 224 | } 225 | return result 226 | } 227 | 228 | func (s *IPMIConfig) GetFreeipmiConfig() string { 229 | var b strings.Builder 230 | if s.Driver != "" { 231 | fmt.Fprintf(&b, "driver-type %s\n", s.Driver) 232 | } 233 | if s.Privilege != "" { 234 | fmt.Fprintf(&b, "privilege-level %s\n", s.Privilege) 235 | } 236 | if s.User != "" { 237 | fmt.Fprintf(&b, "username %s\n", s.User) 238 | } 239 | if s.Password != "" { 240 | fmt.Fprintf(&b, "password %s\n", freeipmi.EscapePassword(s.Password)) 241 | } 242 | if s.Timeout != 0 { 243 | fmt.Fprintf(&b, "session-timeout %d\n", s.Timeout) 244 | } 245 | if len(s.WorkaroundFlags) > 0 { 246 | fmt.Fprintf(&b, "workaround-flags") 247 | for _, flag := range s.WorkaroundFlags { 248 | fmt.Fprintf(&b, " %s", flag) 249 | } 250 | fmt.Fprintln(&b) 251 | } 252 | return b.String() 253 | } 254 | 255 | // ReloadConfig reloads the config in a concurrency-safe way. If the configFile 256 | // is unreadable or unparsable, an error is returned and the old config is kept. 257 | func (sc *SafeConfig) ReloadConfig(configFile string) error { 258 | var c = &Config{} 259 | var config []byte 260 | var err error 261 | 262 | if configFile != "" { 263 | config, err = os.ReadFile(configFile) 264 | if err != nil { 265 | logger.Error("Error reading config file", "error", err) 266 | return err 267 | } 268 | } else { 269 | config = []byte("# use empty file as default") 270 | } 271 | 272 | if err = yaml.Unmarshal(config, c); err != nil { 273 | return err 274 | } 275 | 276 | sc.Lock() 277 | sc.C = c 278 | sc.Unlock() 279 | 280 | if configFile != "" { 281 | logger.Info("Loaded config file", "path", configFile) 282 | } 283 | return nil 284 | } 285 | 286 | // HasModule returns true if a given module is configured. It is concurrency-safe. 287 | func (sc *SafeConfig) HasModule(module string) bool { 288 | sc.Lock() 289 | defer sc.Unlock() 290 | 291 | _, ok := sc.C.Modules[module] 292 | return ok 293 | } 294 | 295 | // ConfigForTarget returns the config for a given target/module, or the 296 | // default. It is concurrency-safe. 297 | func (sc *SafeConfig) ConfigForTarget(target, module string) IPMIConfig { 298 | sc.Lock() 299 | defer sc.Unlock() 300 | 301 | var config IPMIConfig 302 | var ok = false 303 | 304 | if module != "default" { 305 | config, ok = sc.C.Modules[module] 306 | if !ok { 307 | logger.Error("Requested module not found, using default", "module", module, "target", targetName(target)) 308 | } 309 | } 310 | 311 | // If nothing found, fall back to defaults 312 | if !ok { 313 | config, ok = sc.C.Modules["default"] 314 | if !ok { 315 | // This is probably fine for running locally, so not making this a warning 316 | logger.Debug("Needed default config for, but none configured, using FreeIPMI defaults", "target", targetName(target)) 317 | config = defaultConfig 318 | } 319 | } 320 | 321 | return config 322 | } 323 | -------------------------------------------------------------------------------- /contrib/rpm/README.md: -------------------------------------------------------------------------------- 1 | # Building a RPM Package 2 | 3 | The RPM package build targets to run the exporter locally as Prometheus user with sudo permissions to expose most metrics. 4 | 5 | For building a RPM package a build script and [Docker](https://www.docker.com/) build container are available. 6 | 7 | NOTE: 8 | > The build script and the Docker build image must be executed from the project base directory! 9 | 10 | ## CentOS with rpmbuild 11 | 12 | A Build script is located in `contrib/rpm/build.sh` to be executed on a CentOS-based host with rpmbuild tool. 13 | 14 | The RPM package will be available under `$HOME/rpmbuild/`. 15 | 16 | ## Docker Build Container 17 | 18 | A Docker build container is provided for CentOS7. 19 | 20 | ```bash 21 | sudo docker build -t centos7_rpmbuild_ipmi_exporter -f contrib/rpm/docker/Dockerfile-centos7 . 22 | sudo docker run -v $PWD/contrib/rpm/build:/outdir -it centos7_rpmbuild_ipmi_exporter 23 | ``` 24 | 25 | The RPM package will be available under `contrib/rpm/build/`. 26 | -------------------------------------------------------------------------------- /contrib/rpm/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | function do_checks { 5 | 6 | info_msg="The build script must be executed from the projects base directory!" 7 | 8 | if [ -z "$VERSION" ]; then 9 | echo "ERROR: Build failed! VERSION file not found" >&2 10 | echo "INFO: $info_msg" 11 | exit 1 12 | fi 13 | 14 | if [ ! -d "$CONTRIB_DIR" ]; then 15 | echo "ERROR: Build failed! Directory does not exist: $CONTRIB_DIR" >&2 16 | echo "INFO: $info_msg" 17 | exit 1 18 | fi 19 | 20 | } 21 | 22 | export VERSION=$(cat VERSION) 23 | export BUILD_DIR=$HOME/rpmbuild 24 | export CONTRIB_DIR="contrib/rpm" 25 | export PACKAGE_DIR=prometheus-ipmi-exporter-$VERSION 26 | 27 | do_checks 28 | 29 | make build 30 | 31 | mkdir -p $BUILD_DIR/{BUILD,RPMS,SOURCES,SPECS,SRPMS} 32 | mkdir -p $BUILD_DIR/SOURCES/$PACKAGE_DIR/usr/bin 33 | mkdir -p $BUILD_DIR/SOURCES/$PACKAGE_DIR/usr/lib/systemd/system 34 | mkdir -p $BUILD_DIR/SOURCES/$PACKAGE_DIR/etc/sysconfig 35 | mkdir -p $BUILD_DIR/SOURCES/$PACKAGE_DIR/etc/sudoers.d 36 | 37 | sed "s/VERSION/$VERSION/" $CONTRIB_DIR/prometheus-ipmi-exporter.spec > $BUILD_DIR/SPECS/prometheus-ipmi-exporter.spec 38 | 39 | cp $CONTRIB_DIR/systemd/prometheus-ipmi-exporter.service $BUILD_DIR/SOURCES/$PACKAGE_DIR/usr/lib/systemd/system/ 40 | cp $CONTRIB_DIR/sudoers/prometheus-ipmi-exporter $BUILD_DIR/SOURCES/$PACKAGE_DIR/etc/sudoers.d/ 41 | cp $CONTRIB_DIR/config/prometheus-ipmi-exporter.yml $BUILD_DIR/SOURCES/$PACKAGE_DIR/etc/sysconfig/ 42 | cp ipmi_exporter $BUILD_DIR/SOURCES/$PACKAGE_DIR/usr/bin/ 43 | 44 | cd $BUILD_DIR/SOURCES 45 | tar -czvf $PACKAGE_DIR.tar.gz $PACKAGE_DIR 46 | cd $BUILD_DIR 47 | echo Build dir is: $BUILD_DIR 48 | ls -la $BUILD_DIR/SOURCES 49 | rpmbuild -ba $BUILD_DIR/SPECS/prometheus-ipmi-exporter.spec 50 | -------------------------------------------------------------------------------- /contrib/rpm/config/prometheus-ipmi-exporter.yml: -------------------------------------------------------------------------------- 1 | # Configuration file for ipmi_exporter 2 | 3 | modules: 4 | default: 5 | collectors: 6 | - bmc 7 | - ipmi 8 | - dcmi 9 | - chassis 10 | - sel 11 | collector_cmd: 12 | bmc: sudo 13 | ipmi: sudo 14 | dcmi: sudo 15 | chassis: sudo 16 | sel: sudo 17 | custom_args: 18 | bmc: 19 | - "bmc-info" 20 | ipmi: 21 | - "ipmimonitoring" 22 | dcmi: 23 | - "ipmi-dcmi" 24 | chassis: 25 | - "ipmi-chassis" 26 | sel: 27 | - "ipmi-sel" 28 | -------------------------------------------------------------------------------- /contrib/rpm/docker/Dockerfile-centos7: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | MAINTAINER jknedlik , Gabriele Iannetti 3 | WORKDIR /tmp 4 | RUN yum install -y git make gcc rpm-build which 5 | RUN curl -LO https://go.dev/dl/go1.18.1.linux-amd64.tar.gz 6 | RUN tar -C /usr/local -xvzf go1.18.1.linux-amd64.tar.gz 7 | ENV PATH=$PATH:/usr/local/go/bin 8 | COPY . /go/ipmi_exporter 9 | WORKDIR /go/ipmi_exporter 10 | RUN contrib/rpm/build.sh 11 | RUN mkdir /outdir 12 | ENTRYPOINT ["/bin/sh"] 13 | ENTRYPOINT ["cp"] 14 | CMD ["-r", "/root/rpmbuild/RPMS/x86_64/", "/outdir"] 15 | -------------------------------------------------------------------------------- /contrib/rpm/prometheus-ipmi-exporter.spec: -------------------------------------------------------------------------------- 1 | %define __spec_install_post %{nil} 2 | %define debug_package %{nil} 3 | %define __os_install_post %{_dbpath}/brp-compress 4 | 5 | Name: prometheus-ipmi-exporter 6 | Version: VERSION 7 | Release: 1.0%{?dist} 8 | Summary: Remote IPMI exporter for Prometheus 9 | Group: Monitoring 10 | 11 | License: The MIT License 12 | URL: https://github.com/prometheus-community/ipmi_exporter 13 | Source0: %{name}-%{version}.tar.gz 14 | 15 | Requires(pre): shadow-utils 16 | 17 | Requires(post): systemd 18 | Requires(preun): systemd 19 | Requires(postun): systemd 20 | %{?systemd_requires} 21 | BuildRequires: systemd 22 | 23 | BuildRoot: %{_tmppath}/%{name}-%{version}-1-root 24 | 25 | %description 26 | Remote IPMI exporter for Prometheus 27 | 28 | %prep 29 | %setup -q 30 | 31 | %build 32 | # Empty section. 33 | 34 | %install 35 | rm -rf %{buildroot} 36 | mkdir -p %{buildroot}%{_unitdir}/ 37 | cp usr/lib/systemd/system/%{name}.service %{buildroot}%{_unitdir}/ 38 | 39 | # in builddir 40 | cp -a * %{buildroot} 41 | 42 | %clean 43 | rm -rf %{buildroot} 44 | 45 | %pre 46 | getent group prometheus >/dev/null || groupadd -r prometheus 47 | getent passwd prometheus >/dev/null || \ 48 | useradd -r -g prometheus -d /dev/null -s /sbin/nologin \ 49 | -c "Prometheus exporter user" prometheus 50 | cp etc/sudoers.d/%{name} /etc/sudoers.d/%{name} 51 | exit 0 52 | 53 | %post 54 | systemctl enable %{name}.service 55 | systemctl start %{name}.service 56 | 57 | %preun 58 | %systemd_preun %{name}.service 59 | 60 | %postun 61 | %systemd_postun_with_restart %{name}.service 62 | 63 | %files 64 | %defattr(-,root,root,-) 65 | %config /etc/sysconfig/prometheus-ipmi-exporter.yml 66 | %attr(0440, root, root) /etc/sudoers.d/prometheus-ipmi-exporter 67 | %{_bindir}/ipmi_exporter 68 | %{_unitdir}/%{name}.service 69 | -------------------------------------------------------------------------------- /contrib/rpm/sudoers/prometheus-ipmi-exporter: -------------------------------------------------------------------------------- 1 | # !log_allowed setting should be used instead, but is only supported by version 1.8.29 or higher. 2 | 3 | Defaults:prometheus !syslog 4 | prometheus ALL = NOPASSWD: /usr/sbin/ipmimonitoring,\ 5 | /usr/sbin/ipmi-sensors,\ 6 | /usr/sbin/ipmi-dcmi,\ 7 | /usr/sbin/ipmi-raw,\ 8 | /usr/sbin/bmc-info,\ 9 | /usr/sbin/ipmi-chassis,\ 10 | /usr/sbin/ipmi-sel 11 | -------------------------------------------------------------------------------- /contrib/rpm/systemd/prometheus-ipmi-exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Prometheus IPMI Exporter 3 | Documentation=https://github.com/prometheus-community/ipmi_exporter 4 | 5 | [Service] 6 | ExecStart=/usr/bin/ipmi_exporter --config.file=/etc/sysconfig/prometheus-ipmi-exporter.yml 7 | User=prometheus 8 | Restart=always 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | ipmi_exporter: 4 | build: 5 | context: . 6 | command: --config.file /config.yml 7 | volumes: 8 | - ./ipmi_remote.yml:/config.yml:ro # replace with your own config 9 | ports: 10 | - 9290:9290 # bind on 0.0.0.0 11 | # - 127.0.0.1:9290:9290 # or bind to specific interface 12 | hostname: ipmi_exporter_docker 13 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Simply scraping the standard `/metrics` endpoint will make the exporter emit 4 | local IPMI metrics. If the exporter is running with sufficient privileges, no 5 | special configuration is required. See the [privileges document](privileges.md) 6 | for more details. 7 | 8 | For remote metrics, the general configuration pattern is that of a 9 | [multi-target exporter][multi-target]. Please read that guide to get the general 10 | idea about this approach. 11 | 12 | [multi-target]: https://prometheus.io/docs/guides/multi-target-exporter/ "Understanding and using the multi-target exporter pattern - Prometheus docs" 13 | 14 | We offer this approach as IPMI devices often provide useful information even 15 | while the supervised host is turned off. Also, you can have a single exporter 16 | instance probing many (possibly thousands of) IPMI devices, rather than one 17 | exporter per IPMI device. 18 | 19 | **NOTE:** If you are using remote metrics, but still want to get the local 20 | process metrics from the instance, you must use a `default` module with an 21 | empty collectors list and use other modules for the remote hosts. 22 | 23 | ## IPMI exporter 24 | 25 | The exporter can read a configuration file by setting `config.file` (see 26 | above). To collect local metrics, you might not even need one. For 27 | remote metrics, it must contain at least user names and passwords for IPMI 28 | access to all targets to be scraped. You can additionally specify the IPMI 29 | driver type and privilege level to use (see `man 5 freeipmi.conf` for more 30 | details and possible values). 31 | 32 | The config file supports the notion of "modules", so that different 33 | configurations can be re-used for groups of targets. See the section below on 34 | how to set the module parameter in Prometheus. The special module "default" is 35 | used in case the scrape does not request a specific module. 36 | 37 | The configuration file also supports a blacklist of sensors, useful in case of 38 | OEM-specific sensors that FreeIPMI cannot deal with properly or otherwise 39 | misbehaving sensors. This applies to both local and remote metrics. 40 | 41 | There are two commented example configuration files, see `ipmi_local.yml` for 42 | scraping local host metrics and `ipmi_remote.yml` for scraping remote IPMI 43 | interfaces. 44 | 45 | ## Prometheus 46 | 47 | ### Local metrics 48 | 49 | Collecting local IPMI metrics is fairly straightforward. Simply configure your 50 | server to scrape the default metrics endpoint on the hosts running the 51 | exporter. 52 | 53 | ``` 54 | - job_name: ipmi 55 | scrape_interval: 1m 56 | scrape_timeout: 30s 57 | metrics_path: /metrics 58 | scheme: http 59 | static_configs: 60 | - targets: 61 | - 10.1.2.23:9290 62 | - 10.1.2.24:9290 63 | - 10.1.2.25:9290 64 | ``` 65 | 66 | ### Remote metrics 67 | 68 | To add your IPMI targets to Prometheus, you can use any of the supported 69 | service discovery mechanism of your choice. The following example uses the 70 | file-based SD and should be easy to adjust to other scenarios. 71 | 72 | Create a YAML file that contains a list of targets, e.g.: 73 | 74 | ``` 75 | --- 76 | - targets: 77 | - 10.1.2.23 78 | - 10.1.2.24 79 | - 10.1.2.25 80 | - 10.1.2.26 81 | - 10.1.2.27 82 | - 10.1.2.28 83 | - 10.1.2.29 84 | - 10.1.2.30 85 | labels: 86 | job: ipmi_exporter 87 | ``` 88 | 89 | This file needs to be stored on the Prometheus server host. Assuming that this 90 | file is called `/srv/ipmi_exporter/targets.yml`, and the IPMI exporter is 91 | running on a host that has the DNS name `ipmi-exporter.internal.example.com`, 92 | add the following to your Prometheus config: 93 | 94 | ``` 95 | - job_name: ipmi 96 | params: 97 | module: ['default'] 98 | scrape_interval: 1m 99 | scrape_timeout: 30s 100 | metrics_path: /ipmi 101 | scheme: http 102 | file_sd_configs: 103 | - files: 104 | - /srv/ipmi_exporter/targets.yml 105 | refresh_interval: 5m 106 | relabel_configs: 107 | - source_labels: [__address__] 108 | separator: ; 109 | regex: (.*) 110 | target_label: __param_target 111 | replacement: ${1} 112 | action: replace 113 | - source_labels: [__param_target] 114 | separator: ; 115 | regex: (.*) 116 | target_label: instance 117 | replacement: ${1} 118 | action: replace 119 | - separator: ; 120 | regex: .* 121 | target_label: __address__ 122 | replacement: ipmi-exporter.internal.example.com:9290 123 | action: replace 124 | ``` 125 | 126 | This assumes that all hosts use the default module. If you are using modules in 127 | the config file, like in the provided `ipmi_remote.yml` example config, you 128 | will need to specify on job for each module, using the respective group of 129 | targets. 130 | 131 | In a more extreme case, for example if you are using different passwords on 132 | every host, a good approach is to generate an exporter config file that uses 133 | the target name as module names, which would allow you to have single job that 134 | uses label replace to set the module. Leave out the `params` in the job 135 | definition and instead add a relabel rule like this one: 136 | 137 | ``` 138 | - source_labels: [__address__] 139 | separator: ; 140 | regex: (.*) 141 | target_label: __param_module 142 | replacement: ${1} 143 | action: replace 144 | ``` 145 | 146 | For more information, e.g. how to use mechanisms other than a file to discover 147 | the list of hosts to scrape, please refer to the [Prometheus 148 | documentation](https://prometheus.io/docs). 149 | -------------------------------------------------------------------------------- /docs/metrics.md: -------------------------------------------------------------------------------- 1 | # Exported metrics 2 | 3 | ## Exporter meta data 4 | 5 | - `ipmi_exporter_build_info`: standard build meta data provided by the 6 | Prometheus `client_golang` package 7 | 8 | A metric with a constant '1' value labeled by version, revision, branch, 9 | goversion from which the exporter was built, and the goos and goarch for the 10 | build. 11 | 12 | ## Scrape meta data 13 | 14 | These metrics provide data about the scrape itself: 15 | 16 | - `ipmi_up{collector=""}` is `1` if the data for this collector could 17 | successfully be retrieved from the remote host, `0` otherwise. The following 18 | collectors are available and can be enabled or disabled in the config: 19 | - `ipmi`: collects IPMI sensor data. If it fails, sensor metrics (see below) 20 | will not be available 21 | - `dcmi`: collects DCMI data, currently only power consumption. If it fails, 22 | power consumption metrics (see below) will not be available 23 | - `bmc`: collects BMC details. If it fails, BMC info metrics (see below) 24 | will not be available 25 | - `bmc-watchdog`: collects status of the watchdog. If it fails, BMC watchdog 26 | metrics (see below) will not be available 27 | - `chassis`: collects the current chassis power state (on/off). If it fails, 28 | the chassis power state metric (see below) will not be available 29 | - `sel`: collects system event log (SEL) details. If it fails, SEL metrics 30 | (see below) will not be available 31 | - `sel-events`: collects metrics for user-defined events in system event log 32 | (SEL). If it fails, SEL entries metrics (see below) will not be available 33 | - `sm-lan-mode`: collects the "LAN mode" setting in the current BMC config. 34 | If it fails, the LAN mode metric (see below) will not be available 35 | - `ipmi_scrape_duration_seconds` is the amount of time it took to retrieve the 36 | data 37 | 38 | ## BMC info 39 | 40 | This metric is only provided if the `bmc` collector is enabled. 41 | 42 | For some basic information, there is a constant metric `ipmi_bmc_info` with 43 | value `1` and labels providing the firmware revision and manufacturer as 44 | returned from the BMC, and the host system's firmware version (usually the BIOS 45 | version). Example: 46 | 47 | ipmi_bmc_info{firmware_revision="1.66",manufacturer_id="Dell Inc. (674)",system_firmware_version="2.6.1"} 1 48 | 49 | **Note:** some systems do not expose the system's firmware version, in which 50 | case it will be exported as `"N/A"`. 51 | 52 | ## BMC Watchdog 53 | 54 | These metrics are only provided if the `bmc-watchdog` collector is enabled. 55 | 56 | The metric `ipmi_bmc_watchdog_timer_state` shows whether the watchdog timer is 57 | currently running (1) or stopped (0). 58 | 59 | The metric `ipmi_bmc_watchdog_timer_use_state` shows which timer use is 60 | currently active. Per freeipmi bmc-watchdog manual there are 5 uses. This metric 61 | will return 1 for only one of those and 0 for the rest. 62 | 63 | ipmi_bmc_watchdog_timer_use_state{name="BIOS FRB2"} 1 64 | ipmi_bmc_watchdog_timer_use_state{name="BIOS POST"} 0 65 | ipmi_bmc_watchdog_timer_use_state{name="OEM"} 0 66 | ipmi_bmc_watchdog_timer_use_state{name="OS LOAD"} 0 67 | ipmi_bmc_watchdog_timer_use_state{name="SMS/OS"} 0 68 | 69 | The metric `ipmi_bmc_watchdog_logging_state` shows whether the watchdog logging 70 | is enabled (1) or not (0). (Note: This is reversed in freeipmi where 0 enables 71 | logging and 1 disables it) 72 | 73 | The metric `ipmi_bmc_watchdog_timeout_action_state` shows whether watchdog will 74 | take an action on timeout, and if so which one. Per freeipmi bmc-watchdog manual 75 | there are 3 actions. If no action is configured it will be reported as `None`. 76 | 77 | ipmi_bmc_watchdog_timeout_action_state{action="Hard Reset"} 0 78 | ipmi_bmc_watchdog_timeout_action_state{action="None"} 0 79 | ipmi_bmc_watchdog_timeout_action_state{action="Power Cycle"} 1 80 | ipmi_bmc_watchdog_timeout_action_state{action="Power Down"} 0 81 | 82 | The metric `ipmi_bmc_watchdog_timeout_action_state` shows whether a pre-timeout 83 | interrupt is currently active and if so, which one. Per freeipmi bmc-watchdog 84 | manual there are 3 interrupts. If no interrupt is configured it will be reported 85 | as `None`. 86 | 87 | ipmi_bmc_watchdog_pretimeout_interrupt_state{interrupt="Messaging Interrupt"} 0 88 | ipmi_bmc_watchdog_pretimeout_interrupt_state{interrupt="NMI / Diagnostic Interrupt"} 0 89 | ipmi_bmc_watchdog_pretimeout_interrupt_state{interrupt="None"} 1 90 | ipmi_bmc_watchdog_pretimeout_interrupt_state{interrupt="SMI"} 0 91 | 92 | The metric `ipmi_bmc_watchdog_pretimeout_interval_seconds` shows the current 93 | pre-timeout interval as measured in seconds. 94 | 95 | The metric `ipmi_bmc_watchdog_initial_countdown_seconds` shows the configured 96 | countdown in seconds. 97 | 98 | The metric `ipmi_bmc_watchdog_current_countdown_seconds` shows the current 99 | countdown in seconds. 100 | 101 | ## Chassis Power State 102 | 103 | This metric is only provided if the `chassis` collector is enabled. 104 | 105 | The metric `ipmi_chassis_power_state` shows the current chassis power state of 106 | the machine. The value is 1 for power on, and 0 otherwise. 107 | 108 | ## Power consumption 109 | 110 | This metric is only provided if the `dcmi` collector is enabled. 111 | 112 | The metric `ipmi_dcmi_power_consumption_current_watts` can be used to monitor 113 | the live power consumption of the machine in Watts. If in doubt, this metric 114 | should be used over any of the sensor data (see below), even if their name 115 | might suggest that they measure the same thing. This metric has no labels. 116 | 117 | ## System event log (SEL) info 118 | 119 | These metrics are only provided if the `sel` collector is enabled (it isn't by 120 | default). 121 | 122 | The metric `ipmi_sel_entries_count` contains the current number of entries in 123 | the SEL. It is a gauge, as the SEL can be cleared at any time. This metric has 124 | no labels. 125 | 126 | The metric `ipmi_sel_free_space_bytes` contains the current number of free 127 | space for new SEL entries, in bytes. This metric has no labels. 128 | 129 | ## System event log (SEL) entries metrics 130 | 131 | These metrics are only provided if the `sel-events` collector is enabled (it 132 | isn't by default). 133 | 134 | For each event specified in the configuration file (`sel_events` field), will be 135 | generated metrics containing the number of such events and the timestamp of their 136 | last occurrence. Example: 137 | 138 | ipmi_sel_events_count_by_name{name="my_custom_event_from_config"} 77 139 | ipmi_sel_events_latest_timestamp{name="my_custom_event_from_config"} 1.703613275e+09 140 | 141 | also next aggregated metrics will be exported: 142 | 143 | ipmi_sel_events_count_by_state{state="Nominal"} 10 144 | ipmi_sel_events_count_by_state{state="Warning"} 5 145 | 146 | ## Supermicro LAN mode setting 147 | 148 | This metric is only provided if the `sm-lan-mode` collector is enabled (it 149 | isn't by default). 150 | 151 | **NOTE:** This is a vendor-specific collector, it will only work on Supermicro 152 | hardware, possibly even only on _some_ Supermicro systems. 153 | 154 | **NOTE:** Retrieving this setting requires setting `privilege: "admin"` in the 155 | config. 156 | 157 | See e.g. https://www.supermicro.com/support/faqs/faq.cfm?faq=28159 158 | 159 | The metric `ipmi_config_lan_mode` contains the value for the current "LAN mode" 160 | setting (see link above): `0` for "dedicated", `1` for "shared", and `2` for 161 | "failover". 162 | 163 | ## Sensors 164 | 165 | These metrics are only provided if the `ipmi` collector is enabled. 166 | 167 | IPMI sensors in general have one or two distinct pieces of information that are 168 | of interest: a value and/or a state. The exporter always exports both, even if 169 | the value is NaN or the state non-sensical. This is so one can still always 170 | find the metrics to avoid ending up in a situation where one is looking for 171 | e.g. the value of a sensor that is in a critical state, but can't find it and 172 | assume this to be a problem. 173 | 174 | The state of a sensor can be one of _nominal_, _warning_, _critical_, or _N/A_, 175 | reflected by the metric values `0`, `1`, `2`, and `NaN` respectively. Think of 176 | this as a kind of severity. 177 | 178 | For sensors with known semantics (i.e. units), corresponding specific metrics 179 | are exported. For everything else, generic metrics are exported. 180 | 181 | ### Temperature sensors 182 | 183 | Temperature sensors measure a temperature in degrees Celsius and their state 184 | usually reflects the temperature going above the vendor-recommended value. For 185 | each temperature sensor, two metrics are exported (state and value), using the 186 | sensor ID and the sensor name as labels. Example: 187 | 188 | ipmi_temperature_celsius{id="18",name="Inlet Temp"} 24 189 | ipmi_temperature_state{id="18",name="Inlet Temp"} 0 190 | 191 | ### Fan speed sensors 192 | 193 | Fan speed sensors measure fan speed in rotations per minute (RPM) or as a 194 | percentage of the maximum speed, and their state usually reflects the speed 195 | being to low, indicating the fan might be broken. For each fan speed sensor, 196 | two metrics are exported (state and value), using the sensor ID and the 197 | sensor name as labels. Example: 198 | 199 | ipmi_fan_speed_rpm{id="12",name="Fan1A"} 4560 200 | ipmi_fan_speed_state{id="12",name="Fan1A"} 0 201 | 202 | or, for a percentage based fan: 203 | 204 | ipmi_fan_speed_ratio{id="58",name="Fan 1 DutyCycle"} 0.2195 205 | ipmi_fan_speed_state{id="58",name="Fan 1 DutyCycle"} 0 206 | 207 | ### Voltage sensors 208 | 209 | Voltage sensors measure a voltage in Volts. For each voltage sensor, two 210 | metrics are exported (state and value), using the sensor ID and the sensor name 211 | as labels. Example: 212 | 213 | ipmi_voltage_state{id="2416",name="12V"} 0 214 | ipmi_voltage_volts{id="2416",name="12V"} 12 215 | 216 | ### Current sensors 217 | 218 | Current sensors measure a current in Amperes. For each current sensor, two 219 | metrics are exported (state and value), using the sensor ID and the sensor name 220 | as labels. Example: 221 | 222 | ipmi_current_state{id="83",name="Current 1"} 0 223 | ipmi_current_amperes{id="83",name="Current 1"} 0 224 | 225 | ### Power sensors 226 | 227 | Power sensors measure power in Watts. For each power sensor, two metrics are 228 | exported (state and value), using the sensor ID and the sensor name as labels. 229 | Example: 230 | 231 | ipmi_power_state{id="90",name="Pwr Consumption"} 0 232 | ipmi_power_watts{id="90",name="Pwr Consumption"} 70 233 | 234 | Note that based on our observations, this may or may not be a reading 235 | reflecting the actual live power consumption. We recommend using the more 236 | explicit [power consumption metrics](#power_consumption) for this. 237 | 238 | ### Generic sensors 239 | 240 | For all sensors that can not be classified, two generic metrics are exported, 241 | the state and the value. However, to provide a little more context, the sensor 242 | type is added as label (in addition to name and ID). Example: 243 | 244 | ipmi_sensor_state{id="139",name="Power Cable",type="Cable/Interconnect"} 0 245 | ipmi_sensor_value{id="139",name="Power Cable",type="Cable/Interconnect"} NaN 246 | -------------------------------------------------------------------------------- /docs/native.md: -------------------------------------------------------------------------------- 1 | # Go-native IPMI implementation 2 | 3 | The IPMI exporter now supports using a Go-native IPMI implementation, powered 4 | by [go-ipmi](https://github.com/bougou/go-ipmi). In doing so, the exporter can 5 | run *without* the FreeIPMI suite of tools. This mode of operation is currently 6 | considered experimental. But it is being actively maintained/developed, and is 7 | likely the future of this exporter. 8 | 9 | ## Should I use it? 10 | 11 | In general, if you have the time to spare, it would be great if you could give 12 | this a spin and provide 13 | [feedback](https://github.com/prometheus-community/ipmi_exporter/issues) if you 14 | can spot anything is _not_ working in native-IPMI mode that _is_ working via 15 | FreeIPMI (the default mode). 16 | 17 | Besides that, the native implementation also offers some real benefits: 18 | 19 | * No more execution of external commands 20 | * If you are affected by #227 - this cannot happen with native IPMI 21 | * Some collectors may require less round-trips, as the exporter has more 22 | control over the IPMI calls being made 23 | * The BMC watchdog collector now works remotely 24 | * In the future, as the native implementation matures, it might provide better 25 | data in certain situations 26 | 27 | ## How do I use it? 28 | 29 | Simply run the exporter with `--native-ipmi`. But please make sure to read the 30 | rest of this document. 31 | 32 | ## What to watch out for? 33 | 34 | There are some subtle differences to be aware of, compared to the 35 | FreeIPMI-based collectors: 36 | 37 | * **All collectors:** 38 | * The following config items no longer have any effect: 39 | * `driver` (only local and `LAN_2_0` are supported, please open an issue if 40 | you rely on another driver) 41 | * `workaround_flags` (not supported by go-ipmi, please open an issue if you 42 | rely on this) 43 | * `collector_cmd`, `collector_args`, `custom_cmd` - no longer applicable, 44 | please see also privileges section below 45 | * The `privilege` config item should no longer be needed (FreeIPMI restricts 46 | this to "OPERATOR" by default, but go-ipmi does not) 47 | * **ipmi collector:** sensors can now have a `state` value of `3` 48 | ("non-recoverable") - a value that FreeIPMI does not provide 49 | * **chassis collector:** in the native collector, the representation changed 50 | from `"Current drive fault state (1=false, 0=true)."` to `"Current drive 51 | fault state (1=true, 0=false)."`, simply because the current representation 52 | is weird and will likely also be changed in a future major release; same 53 | thing for the fan fault state 54 | * **bmc collector:** this needs some testing, specifically the 55 | `system_firmware_revision`, as not all hardware supports this 56 | 57 | ## Privileges 58 | 59 | Since no external commands are executed in native IPMI mode, none of the `sudo` 60 | trickery described in 61 | [privileges](https://github.com/prometheus-community/ipmi_exporter/blob/master/docs/privileges.md) 62 | will work anymore. Make sure the exporters runs as a user that has access to 63 | the local IPMI device (for local scraping, for remote scraping no special 64 | privileges should be required) 65 | 66 | ## Feedback 67 | 68 | Please [open an 69 | issue](https://github.com/prometheus-community/ipmi_exporter/issues) if you run 70 | into problems using the native IPMI mode. 71 | -------------------------------------------------------------------------------- /docs/privileges.md: -------------------------------------------------------------------------------- 1 | # Privileges 2 | 3 | If you are running the exporter as unprivileged user, but need to execute the 4 | FreeIPMI tools as root (as is likely necessary to access the local IPMI 5 | interface), you can do the following: 6 | 7 | **NOTE:** Make sure to adapt all absolute paths to match your distro! 8 | 9 | 1. Add sudoers files to permit the following commands 10 | ``` 11 | ipmi-exporter ALL = NOPASSWD: /usr/sbin/ipmimonitoring,\ 12 | /usr/sbin/ipmi-sensors,\ 13 | /usr/sbin/ipmi-dcmi,\ 14 | /usr/sbin/ipmi-raw,\ 15 | /usr/sbin/bmc-info,\ 16 | /usr/sbin/ipmi-chassis,\ 17 | /usr/sbin/ipmi-sel 18 | ``` 19 | 2. In your module config, override the collector command with `sudo` for 20 | every collector you are using and add the actual command as custom 21 | argument. Example for the "ipmi" collector: 22 | ```yaml 23 | collector_cmd: 24 | ipmi: /usr/bin/sudo 25 | custom_args: 26 | ipmi: 27 | - "/usr/sbin/ipmimonitoring" 28 | ``` 29 | See also the [sudo example config](../ipmi_local_sudo.yml). 30 | 31 | Note that no elevated privileges are necessary for getting remote metrics. 32 | -------------------------------------------------------------------------------- /freeipmi/freeipmi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package freeipmi 15 | 16 | import ( 17 | "bufio" 18 | "bytes" 19 | "crypto/rand" 20 | "encoding/csv" 21 | "encoding/hex" 22 | "fmt" 23 | "log/slog" 24 | "math" 25 | "os" 26 | "os/exec" 27 | "path/filepath" 28 | "regexp" 29 | "strconv" 30 | "strings" 31 | "syscall" 32 | ) 33 | 34 | var ( 35 | ipmiDCMIPowerMeasurementRegex = regexp.MustCompile(`^Power Measurement\s*:\s*(?PActive|Not\sAvailable).*`) 36 | ipmiDCMICurrentPowerRegex = regexp.MustCompile(`^Current Power\s*:\s*(?P[0-9.]*)\s*Watts.*`) 37 | ipmiChassisPowerRegex = regexp.MustCompile(`^System Power\s*:\s(?P.*)`) 38 | ipmiChassisDriveFaultRegex = regexp.MustCompile(`^Drive Fault\s*:\s(?P.*)`) 39 | ipmiChassisCoolingFaultRegex = regexp.MustCompile(`^Cooling/fan fault\s*:\s(?P.*)`) 40 | ipmiSELEntriesRegex = regexp.MustCompile(`^Number of log entries\s*:\s(?P[0-9.]*)`) 41 | ipmiSELFreeSpaceRegex = regexp.MustCompile(`^Free space remaining\s*:\s(?P[0-9.]*)\s*bytes.*`) 42 | ipmiSELEventRegex = regexp.MustCompile(`^(?P[0-9]+),\s*(?P[^,]*),(?P