├── .circleci └── config.yml ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE.md ├── 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 ├── auth-split-migration.md ├── collector ├── collector.go └── collector_test.go ├── config └── config.go ├── config_test.go ├── examples └── systemd │ └── snmp_exporter.service ├── generator ├── Dockerfile ├── Dockerfile-local ├── FORMAT.md ├── Makefile ├── README.md ├── config.go ├── generator.yml ├── main.go ├── mibs │ └── .do_not_remove ├── net_snmp.go ├── tree.go └── tree_test.go ├── go.mod ├── go.sum ├── main.go ├── scraper ├── gosnmp.go ├── mock.go └── scraper.go ├── snmp-mixin ├── Makefile ├── README.md ├── alerts │ ├── snmp_general.yml │ └── snmp_ubiquiti_wifi.yml ├── dashboards │ ├── snmp_ubiquiti_access_point.json │ └── snmp_ubiquiti_wifi.json ├── mixin.libsonnet └── rules │ └── rules.yaml ├── snmp.yml └── testdata ├── snmp-auth-envvars.yml ├── snmp-auth-v2nocreds.yml ├── snmp-auth.yml └── snmp-with-overrides.yml /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | 4 | orbs: 5 | prometheus: prometheus/prometheus@0.17.1 6 | 7 | executors: 8 | # Whenever the Go version is updated here, .promu.yml 9 | # should also be updated. 10 | golang: 11 | docker: 12 | - image: cimg/go:1.24 13 | parameters: 14 | working_dir: 15 | type: string 16 | default: ~/project 17 | working_directory: << parameters.working_dir >> 18 | 19 | jobs: 20 | test: 21 | executor: golang 22 | 23 | steps: 24 | - prometheus/setup_environment 25 | - run: sudo apt-get update 26 | - run: sudo apt-get -y install build-essential libsnmp-dev 27 | - run: make 28 | - run: git diff --exit-code 29 | - prometheus/store_artifact: 30 | file: snmp_exporter 31 | 32 | generator: 33 | executor: 34 | name: golang 35 | working_dir: ~/project/generator 36 | 37 | environment: 38 | MIBDIRS: mibs 39 | steps: 40 | - checkout: 41 | path: ~/project 42 | - run: sudo apt-get update 43 | - run: sudo apt-get -y install build-essential diffutils libsnmp-dev p7zip-full 44 | - run: make mibs 45 | - run: make generator 46 | - run: make parse_errors 47 | - run: make generate 48 | - run: diff -u ../snmp.yml snmp.yml 49 | 50 | publish_generator_main: 51 | executor: golang 52 | 53 | steps: 54 | - prometheus/setup_build_environment: 55 | docker_version: "" 56 | - run: make -C generator docker REPO_TAG=main DOCKER_IMAGE_TAG=main 57 | - run: make -C generator docker REPO_TAG=main DOCKER_REPO=quay.io/prometheus DOCKER_IMAGE_TAG=main 58 | - run: docker images 59 | - run: docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD 60 | - run: docker login -u $QUAY_LOGIN -p $QUAY_PASSWORD quay.io 61 | - run: make -C generator docker-publish DOCKER_IMAGE_TAG=main 62 | - run: make -C generator docker-publish DOCKER_REPO=quay.io/prometheus DOCKER_IMAGE_TAG=main 63 | 64 | publish_generator_release: 65 | executor: golang 66 | 67 | steps: 68 | - prometheus/setup_build_environment: 69 | docker_version: "" 70 | - run: make -C generator docker REPO_TAG=$CIRCLE_TAG DOCKER_IMAGE_TAG=$CIRCLE_TAG 71 | - run: make -C generator docker REPO_TAG=$CIRCLE_TAG DOCKER_IMAGE_TAG=$CIRCLE_TAG DOCKER_REPO=quay.io/prometheus 72 | - run: docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD 73 | - run: docker login -u $QUAY_LOGIN -p $QUAY_PASSWORD quay.io 74 | - run: | 75 | if [[ "$CIRCLE_TAG" =~ ^v[0-9]+(\.[0-9]+){2}$ ]]; then 76 | make -C generator docker-tag-latest DOCKER_IMAGE_TAG="$CIRCLE_TAG" 77 | make -C generator docker-tag-latest DOCKER_IMAGE_TAG="$CIRCLE_TAG" DOCKER_REPO=quay.io/prometheus 78 | fi 79 | - run: make -C generator docker-publish DOCKER_IMAGE_TAG=$CIRCLE_TAG 80 | - run: make -C generator docker-publish DOCKER_IMAGE_TAG=$CIRCLE_TAG DOCKER_REPO=quay.io/prometheus 81 | - run: | 82 | if [[ "$CIRCLE_TAG" =~ ^v[0-9]+(\.[0-9]+){2}$ ]]; then 83 | make -C generator docker-publish DOCKER_IMAGE_TAG="latest" 84 | make -C generator docker-publish DOCKER_IMAGE_TAG="latest" DOCKER_REPO=quay.io/prometheus 85 | fi 86 | 87 | mixin: 88 | executor: golang 89 | 90 | steps: 91 | - checkout 92 | - run: go install github.com/monitoring-mixins/mixtool/cmd/mixtool@latest 93 | - run: go install github.com/google/go-jsonnet/cmd/jsonnetfmt@latest 94 | - run: make -C snmp-mixin lint build 95 | 96 | workflows: 97 | version: 2 98 | snmp_exporter: 99 | jobs: 100 | - test: 101 | filters: 102 | tags: 103 | only: /.*/ 104 | - mixin: 105 | filters: 106 | tags: 107 | only: /.*/ 108 | - prometheus/build: 109 | name: build 110 | filters: 111 | tags: 112 | only: /.*/ 113 | - generator: 114 | filters: 115 | tags: 116 | only: /.*/ 117 | - prometheus/publish_main: 118 | context: org-context 119 | requires: 120 | - test 121 | - build 122 | - generator 123 | filters: 124 | branches: 125 | only: main 126 | - prometheus/publish_release: 127 | context: org-context 128 | requires: 129 | - test 130 | - build 131 | - generator 132 | filters: 133 | tags: 134 | only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ 135 | branches: 136 | ignore: /.*/ 137 | - publish_generator_main: 138 | context: org-context 139 | requires: 140 | - test 141 | - build 142 | - generator 143 | filters: 144 | branches: 145 | only: main 146 | - publish_generator_release: 147 | context: org-context 148 | requires: 149 | - test 150 | - build 151 | - generator 152 | filters: 153 | tags: 154 | only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ 155 | branches: 156 | ignore: /.*/ 157 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .tarballs/ 3 | 4 | !.build/linux-amd64/ 5 | !.build/linux-armv7 6 | !.build/linux-arm64 7 | !.build/linux-ppc64le 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | ### Host operating system: output of `uname -a` 18 | 19 | ### snmp_exporter version: output of `snmp_exporter -version` 20 | 21 | 22 | ### What device/snmpwalk OID are you using? 23 | 24 | ### If this is a new device, please link to the MIB(s). 25 | 26 | ### What did you do that produced an error? 27 | 28 | ### What did you expect to see? 29 | 30 | ### What did you see instead? 31 | -------------------------------------------------------------------------------- /.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 | with: 23 | persist-credentials: false 24 | - name: Set docker hub repo name 25 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 26 | - name: Push README to Dockerhub 27 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 28 | env: 29 | DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} 30 | DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }} 31 | with: 32 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 33 | provider: dockerhub 34 | short_description: ${{ env.DOCKER_REPO_NAME }} 35 | # Empty string results in README-containers.md being pushed if it 36 | # exists. Otherwise, README.md is pushed. 37 | readme_file: '' 38 | 39 | PushQuayIoReadme: 40 | runs-on: ubuntu-latest 41 | name: Push README to quay.io 42 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 43 | steps: 44 | - name: git checkout 45 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 46 | with: 47 | persist-credentials: false 48 | - name: Set quay.io org name 49 | run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV 50 | - name: Set quay.io repo name 51 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 52 | - name: Push README to quay.io 53 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 54 | env: 55 | DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} 56 | with: 57 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 58 | provider: quay 59 | # Empty string results in README-containers.md being pushed if it 60 | # exists. Otherwise, README.md is pushed. 61 | readme_file: '' 62 | -------------------------------------------------------------------------------- /.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@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 30 | with: 31 | go-version: 1.24.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@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 37 | with: 38 | args: --verbose 39 | version: v2.1.5 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | dependencies-stamp 24 | /snmp_exporter 25 | /.build 26 | /.release 27 | /.tarballs 28 | .deps 29 | *.tar.gz 30 | /vendor 31 | 32 | generator/generator 33 | generator/mibs 34 | generator/snmp.yml 35 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - misspell 5 | - revive 6 | - sloglint 7 | disable: 8 | - unused 9 | settings: 10 | errcheck: 11 | exclude-functions: 12 | - (net/http.ResponseWriter).Write 13 | revive: 14 | rules: 15 | - name: unused-parameter 16 | severity: warning 17 | disabled: true 18 | exclusions: 19 | generated: lax 20 | presets: 21 | - comments 22 | - common-false-positives 23 | - legacy 24 | - std-error-handling 25 | rules: 26 | - linters: 27 | - errcheck 28 | - govet 29 | path: _test.go 30 | paths: 31 | - third_party$ 32 | - builtin$ 33 | - examples$ 34 | formatters: 35 | exclusions: 36 | generated: lax 37 | paths: 38 | - third_party$ 39 | - builtin$ 40 | - examples$ 41 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | # Whenever the Go version is updated here, 3 | # .circle/config.yml should also be updated. 4 | version: 1.24 5 | repository: 6 | path: github.com/prometheus/snmp_exporter 7 | build: 8 | ldflags: | 9 | -X github.com/prometheus/common/version.Version={{.Version}} 10 | -X github.com/prometheus/common/version.Revision={{.Revision}} 11 | -X github.com/prometheus/common/version.Branch={{.Branch}} 12 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 13 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 14 | tarball: 15 | files: 16 | - snmp.yml 17 | - LICENSE 18 | - NOTICE 19 | -------------------------------------------------------------------------------- /.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 | ## 0.29.0 / 2025-04-23 2 | 3 | * [CHANGE] generator: fail early for bad walk OID #1411 4 | 5 | snmp.yml changes: 6 | * added Yamaha RT Series module #1369 7 | * updated APC Powernet MIB #1376 8 | * added cisco_device and cisco_fc_fe modules #1367 - #1374 #1375 9 | * added MSA fieldserver gateway module fieldserver #1392 10 | * update Mikrotik MIB to 7.18.2 #1417 11 | * fixed hrDevice override #1398 12 | * update Eltex MIB to 10.3.6.11 #1415 13 | 14 | ## 0.28.0 / 2025-02-07 15 | 16 | BREAKING CHANGES: 17 | 18 | In this version of the exporter the sysUpTime metric has been removed from the if_mib module and 19 | is now part of the new system module, along with other useful system related metrics. 20 | Please update your scrape definitions to include the system module if you need sysUpTime. 21 | 22 | * [ENHANCEMENT] allow module-qualified labels in generator #1333 23 | * [ENHANCEMENT] add healthcheck endpoint #1358 24 | * [ENHANCEMENT] Override Metric Name in Generator #1341 25 | * [BUGFIX] cleanup docker container after running #1330 26 | 27 | snmp.yml changes: 28 | * moved system related oids to the new system module #1334 29 | * add UBNT AirOS module, DLink and Eltex MES #1344 30 | * add JunOS module #1348 31 | * enhancements on the hrStorage module, cleanup unused lookups for mikrotik module #1349 32 | * update JunOS module and add Juniper Optics (DOM) module #1351 33 | * added page counters to the printer module #1353 34 | 35 | ## 0.27.0 / 2025-01-03 36 | BREAKING CHANGES: 37 | 38 | This version of the exporter introduces a cleaned up default snmp.yml that moved all 39 | ucd-snmp-mib oids into a separate module. 40 | 41 | If you used one of the following modules: 42 | * synology 43 | * ddwrt 44 | * kemp_loadmaster 45 | 46 | you will need to change your scrape config to also include the ucd_la_table module as well. 47 | See https://github.com/prometheus/snmp_exporter/tree/main?tab=readme-ov-file#multi-module-handling for further instructions. 48 | 49 | * [CHANGE] generator: Update generator default MIBOPTS #1231 50 | * [CHANGE] adopt log/slog, drop go-kit/log #1249 51 | * [ENHANCEMENT] generator: Improve config error message #1274 52 | * [FEATURE] add ParseDateAndTime type #1234 53 | * [FEATURE] Set UseUnconnectedUDPSocket option if one of the modules has if set #1247 54 | * [FEATURE] add NTPTimeStamp type #1315 55 | * [BUGFIX] fixed dashboard mixins #1319 56 | 57 | snmp.yml changes: 58 | * cleanup ucd-snmp-mibs #1200 59 | * moved oids from synology,ddwrt and kemp_loadmaster to new module ucd_la_table 60 | * Added support for Sophos XG Series #1239 61 | * Added support for HPE #1267 62 | * Added support for powercom #1275 63 | * Added support for Cisco IMC #1293 64 | * Updated mib for apc #1303 65 | * Added support for TPLink DDM #1304 66 | 67 | ## 0.26.0 / 2024-05-08 68 | 69 | * [CHANGE] Improve generator parse error handling #1167 70 | * [ENHANCEMENT] generator: Add generator HELP override #1106 71 | * [ENHANCEMENT] Refactoring of Scrape process, fixing multiple module issues #1111 72 | * [ENHANCEMENT] Skip using an interactive terminal in "make docker-generate". #1113 73 | * [ENHANCEMENT] Add SNMPInflight metric #1119 74 | * [FEATURE] Support for passing username, password & priv_password as env vars #1074 75 | * [FEATURE] Add GoSNMP logger #1157 76 | * [FEATURE] Add a "snmp_context" parameter to the URL #1163 77 | * [BUGFIX] generator: curl failed #1094 78 | * [BUGFIX] Fix SNMPv3 password configuration #1122 79 | * [BUGFIX] generator: Update generator User-Agent #1133 80 | * [BUGFIX] generator: fix mibs directory specification for parse_errors command #1135 81 | * [BUGFIX] generator: remove extra character from dell iDrac-SMIv1 MIB #1141 82 | * [BUGFIX] Fix do not expand envvars for empty config fields #1148 83 | 84 | snmp.yml changes: 85 | * Updated Cisco MIBs #1180 86 | * Updated Cyberpower MIBs #1124 87 | * Updated servertech_sentry3 #1090 88 | * Added support for Dell iDrac #1125 89 | 90 | ## 0.25.0 / 2023-12-10 91 | 92 | * [ENHANCEMENT] generator: Add support for subsequent address family #782 93 | * [ENHANCEMENT] generator: Fix lookups to match OIDs closer to the index OID. #828 94 | * [FEATURE] Add a scaling factor #1026 95 | * [FEATURE] generator: Enable passing input file, output file, and mibs dir as flags #1028 96 | * [FEATURE] Add an offset factor #1029 97 | * [BUGFIX] Fix and optimize generator Docker image building #1045 98 | 99 | snmp.yml changes: 100 | 101 | * Override `bsnAPName` to DisplayString #660 102 | * Import TP-Link EAP MIB #833 103 | * Updated Mikrotik neighbor indexes make them unique #986 104 | * Update PowerNet MIB to v4.5.1 #1003 105 | * Refactor HOST-RESOURCES-MIB #1027 106 | * Update keepalived MIB files to latest version #1044 107 | 108 | ## 0.24.1 / 2023-09-01 109 | 110 | * [BUGFIX] Remove auth label from collection metric #981 111 | 112 | ## 0.24.0 / 2023-08-29 113 | 114 | * [CHANGE] Sanitize invalid UTF-8 #968 115 | * [FEATURE] Support fetching multiple modules in one scrape #945 116 | * [FEATURE] Support loading multiple configuration files #970 117 | 118 | ## 0.23.0 / 2023-07-20 119 | 120 | BREAKING CHANGES: 121 | 122 | This version of the exporter introduces a new configuration file format. This 123 | new format separates the walk and metric mappings from the connection and 124 | authentication settings. This allows for easier configuration of different 125 | auth params without having to duplicate the full walk and metric mapping. 126 | 127 | See auth-split-migration.md for more details. 128 | 129 | * [CHANGE] Split config of auth and modules #859 130 | * [FEATURE] Add support for parsing SNMP transport from target #914 131 | * [ENHANCEMENT] Improved Lookup process for label information #908 132 | * [BUGFIX] Fix metrics path not using command-line argument value #904 133 | 134 | ## 0.22.0 / 2023-06-15 135 | 136 | * [FEATURE] Add indices filters #624 137 | * [FEATURE] Add MIBOPTS flag to the generator #891 138 | * [ENHANCEMENT] Treat Bits as OctetString #870 139 | * [ENHANCEMENT] Report duration in logs for canceled scrapes #876 140 | * [BUGFIX] Fix several generator MIBs. #843, #868, #889 141 | 142 | ## 0.21.0 / 2022-11-22 143 | 144 | * [CHANGE] Update to exporter-toolkit v0.8.1 (#810) 145 | * [FEATURE] Support chained lookups in the generator (#757) 146 | * [ENHANCEMENT] Add per-SNMP packet statistics. (#656) 147 | * [ENHANCEMENT] Add support for aes192c and aes256c privacy protocol (#657) 148 | * [ENHANCEMENT] Support responding from different source address (#702) 149 | * [BUGFIX] Fixes dropped context passing (#634) 150 | * [BUGFIX] Add version flag (#717) 151 | * [BUGFIX] Fix retries in generator (#786) 152 | 153 | ## 0.20.0 / 2021-02-12 154 | 155 | NOTE: This is a safe harbor release. Future releases will have breaking changes to the configuration format. 156 | 157 | * [ENHANCEMENT] Remove vendoring 158 | * [ENHANCEMENT] Add TLS support 159 | 160 | ## 0.19.0 / 2020-08-31 161 | 162 | * [ENHANCEMENT] Support EnumAsInfo as an index (#559) 163 | * [ENHANCEMENT] Allow lookup chaining for uints (#527) 164 | 165 | ## 0.18.0 / 2020-05-26 166 | 167 | * [FEATURE] Allow lookup chaining in a basic way (#489) 168 | * [BUGFIX] Reduce and fix timeouts for SNMP requests (#511) 169 | 170 | ## 0.17.0 / 2020-02-17 171 | 172 | * [ENHANCEMENT] Use contexts with SNMP, so the http connection closing stops the SNMP walk. (#481) 173 | * [ENHANCEMENT] Sanitize the snmp probe query params by only allowing them to be specified once. (#467) 174 | 175 | ## 0.16.1 / 2019-12-10 176 | 177 | * [FEATURE] Support BITS values. (#465) 178 | * [ENHANCEMENT] Add option to fail on parse errors in the generator. (#382) 179 | * [ENHANCEMENT] Switch logging to go-kit (#447) 180 | * [BUGFIX] Handle trailing linefeed in NetSNMP output adding 1 to the error count (#398) 181 | 182 | ## 0.15.0 / 2019-02-12 183 | 184 | This release includes changes to both the generator.yml format and the default output of the generator for lookups. 185 | 186 | * [CHANGE] Support multi-index lookups. This changes old_index to be a list old_indexes in generator.yml. (#339) 187 | * [CHANGE] Allow keeping of old labels from lookups, enabled by default (#339) 188 | * [CHANGE] The previous example modules if_mib_ifalias, if_mib_ifdescr, and if_mib_ifname have been removed from snmp.yml/generator.yml. These labels are now all available on the default if_mib example module (#339) 189 | * [FEATURE] Add EnumAsInfo and EnumAsStateSet type overrides (#378) 190 | * [ENHANCEMENT] Better error messages when an index can't be handled (#369) 191 | 192 | ## 0.14.0 / 2018-12-04 193 | 194 | * [FEATURE] Add dry-run option to validate configuration (#363) 195 | * [FEATURE] Add support for LLDP-MIB's LldpPortId (#337) 196 | * [ENHANCEMENT] Add automatic Cou nter64 wrapping (#351) 197 | * [ENHANCEMENT] Add comment that snmp.yaml is auto-generated (#364) 198 | * [BUGFIX] Fix signal handling (#353) 199 | 200 | ## 0.13.0 / 2018-09-12 201 | 202 | * [FEATURE] Add support for IMPLIED indexes 203 | * [FEATURE] Add support for InetAddress 204 | * [FEATURE] Add support for overriding InetAddress when index size is incorrectly missing, as seen in some Juniper devices 205 | 206 | ## 0.12.0 / 2018-08-15 207 | 208 | * [FEATURE] Support added for DateAndTime textual convention (#322) 209 | * [BUGFIX] Avoid false positives when looking for display strings (#312) 210 | 211 | ## 0.11.0 / 2018-05-30 212 | 213 | * [FEATURE] Generator: Support ignoring specific metrics 214 | * [FEATURE] Generator: Support overriding the type of metrics 215 | * [BUGFIX] Don't panic on invalid utf-8 data, just fail the scrape 216 | 217 | ## 0.10.0 / 2018-04-26 218 | 219 | * [FEATURE] Use GET rather than GETBULK if specific non-table object or table object instance is listed in generator module walk 220 | * [BUGFIX] Better handle SNMP v3 auth config, fixing some validation 221 | * [BUGFIX] Fail the scrape rather than exposing invalid UTF-8 label values 222 | * [BUGFIX] Remove incorrect InetAddress implementation 223 | 224 | ## 0.9.0 / 2018-02-26 225 | 226 | * [FEATURE] Support for Opaque Float/Double types 227 | 228 | ## 0.8.0 / 2017-11-20 229 | 230 | * [FEATURE] Support SNMP v3 context names 231 | * [FEATURE] Support fixed-size string indexes 232 | 233 | ## 0.7.0 / 2017-10-09 234 | 235 | * [FEATURE] Generator detects a broader range of display strings, including SnmpAdminString 236 | * [BUGFIX] Pull in upstream fix for spurious results when walk matched no oids 237 | 238 | ## 0.6.0 / 2017-08-22 239 | 240 | * [CHANGE] Default module is now `if_mib` rather than `default`. `if_mib` has no lookups, and `if_mib_*` has replaced `default_*`. `if_mib_ifdescr` has the old behaviour of `default`. 241 | * [BUGFIX] Don't hide secrets when generating snmp.yml 242 | * [BUGFIX] Correctly handle different auth settings across modules 243 | 244 | ## 0.5.0 / 2017-08-15 245 | 246 | * [FEATURE] Expose config on /config 247 | * [FEATURE] Add help text to metrics 248 | * [FEATURE] Allow for extracting numeric metrics from string objects via regular expressions 249 | * [FEATURE/CHANGE] Config now only reloaded on SIGHUP or /-/reload 250 | * [CHANGE] Switch to kingpin flags, all flags now have two hyphens rather than one 251 | * [CHANGE] Remove Fortinet example module 252 | * [BUGFIX] Handle Counter64s with values >=2^63 correctly 253 | * [BUGFIX] Sanitize metric names 254 | * [BUGFIX] Add back objects marked no-access to generator output 255 | 256 | ## v0.4.0 / 2017-06-06 257 | 258 | * [FEATURE] Add Homepage on /. #135 259 | * [ENHANCEMENT] Add ddwrt OIDs to generator. #147 260 | * [ENHANCEMENT] Add synology OIDs to generator. #149, #154 261 | * [ENHANCEMENT] Use lookup node's index label in the generator. #162 262 | * [BUGFIX] Fix `authNoPriv` in config parsing. #141 263 | * [BUGFIX] Update gosnmp vendoring to fix timeouts/errors. #139, #171 264 | 265 | ## 0.3.0 / 2017-03-15 266 | 267 | * [FEATURE] Support MAC Addresses and IP addresses as object values 268 | * [ENHANCEMENT] Allow compiling generator under FreeBSD 269 | * [ENHANCEMENT] Workaround RFC1213-MIB being too old to have type hints 270 | * [BUGFIX] Represent OctetStrings as hex 271 | 272 | ## 0.2.0 / 2017-01-25 273 | 274 | * [FEATURE] Add config generator 275 | * [FEATURE] Add support for strings in PDUs 276 | * [FEATURE] Add debug logging 277 | * [FEATURE] Add -version flag 278 | * [BUGFIX] Correctly handle missing label lookups 279 | 280 | 281 | ## 0.1.0 / 2016-09-23 282 | 283 | This is a port to Go from the original Python version. 284 | 285 | Behaviour is largely the same (the same config file will work), however the URL has changed a bit: It's now /snmp?target=1.2.3.4 where previously it was /metrics?address=1.2.3.4 286 | 287 | As this is a rewrite, a full list of changes will not be provided. 288 | 289 | 290 | ## 0.0.6 / 2016-08-13 291 | 292 | * [FEATURE] SNMP v1 support 293 | * [FEATURE] SNMP v3 support 294 | * [FEATURE] InetAddress supported as part of a table index 295 | * [FEATURE] OctetString supported as part of a table index 296 | * [FEATURE] Cisco WLC example added to config 297 | * [FEATURE] Example systemd config 298 | * [ENHANCEMENT] Handle devices that remove trailing 0s in OIDs 299 | * [ENHANCEMENT] Python 3 support 300 | * [BUGFIX] Fixed rendering of MAC addresses 301 | 302 | 303 | ## 0.0.5 / 2016-01-30 304 | 305 | This release is breaking. To convert your config to work with the new release, indent all the lines and then prepend the line`default:` 306 | 307 | * [FEATURE] Support multiple modules inside one config 308 | 309 | 310 | ## 0.0.4 / 2016-01-08 311 | 312 | This release changes the SNMP exporter to use the NetSNMP Python bindings, which are faster and use less CPU. 313 | This needs manual installation of the bindings: 314 | 315 | ``` 316 | apt-get install libsnmp-python # On older Debian-based distros. 317 | apt-get install python-netsnmp # On newer Debian-based distros. 318 | yum install net-snmp-python # On Red Hat-based distros. 319 | ``` 320 | 321 | * [FEATURE] Support for setting community 322 | * [ENHANCEMENT] Switch to NetSNMP Python bindings 323 | * [ENHANCEMENT] Rule lookup is done with a tree rather than a linear search 324 | * [ENHANCEMENT] Various tweaks for dodgy hardware 325 | -------------------------------------------------------------------------------- /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 2 | 3 | Prometheus uses GitHub to manage reviews of pull requests. 4 | 5 | * If you have a trivial fix or improvement, go ahead and create a pull request, 6 | addressing (with `@...`) the maintainer of this repository (see 7 | [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. 8 | 9 | * If you plan to do something more involved, first discuss your ideas 10 | on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers). 11 | This will avoid unnecessary work and surely give you and us a good deal 12 | of inspiration. 13 | 14 | * Relevant coding style guidelines are the [Go Code Review 15 | Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) 16 | and the _Formatting and style_ section of Peter Bourgon's [Go: Best 17 | Practices for Production 18 | Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). 19 | 20 | * Be sure to sign off on the [DCO](https://github.com/probot/dco#how-it-works). 21 | 22 | # What to contribute 23 | 24 | The best way to help without speaking a lot of Go would be to share your 25 | configuration, alerts, dashboards, and recording rules. If you have something 26 | that works and is not in the upstream repository, please pay it forward and 27 | share what works. 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest 4 | LABEL maintainer="The Prometheus Authors " 5 | 6 | ARG ARCH="amd64" 7 | ARG OS="linux" 8 | COPY .build/${OS}-${ARCH}/snmp_exporter /bin/snmp_exporter 9 | COPY snmp.yml /etc/snmp_exporter/snmp.yml 10 | 11 | EXPOSE 9116 12 | ENTRYPOINT [ "/bin/snmp_exporter" ] 13 | CMD [ "--config.file=/etc/snmp_exporter/snmp.yml" ] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | * Basti Schubert @bastischubert 2 | * Ben Kochie @SuperQ 3 | * Richard Hartmann @RichiH 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 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 | # Needs to be defined before including Makefile.common to auto-generate targets 15 | DOCKER_ARCHS ?= amd64 armv7 arm64 ppc64le 16 | 17 | include Makefile.common 18 | 19 | STATICCHECK_IGNORE = 20 | 21 | DOCKER_IMAGE_NAME ?= snmp-exporter 22 | 23 | ifdef DEBUG 24 | bindata_flags = -debug 25 | endif 26 | -------------------------------------------------------------------------------- /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 ?= v2.1.5 65 | GOLANGCI_FMT_OPTS ?= 66 | # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. 67 | # windows isn't included here because of the path separator being different. 68 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) 69 | ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) 70 | # If we're in CI and there is an Actions file, that means the linter 71 | # is being run in Actions, so we don't need to run it here. 72 | ifneq (,$(SKIP_GOLANGCI_LINT)) 73 | GOLANGCI_LINT := 74 | else ifeq (,$(CIRCLE_JOB)) 75 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 76 | else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) 77 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 78 | endif 79 | endif 80 | endif 81 | 82 | PREFIX ?= $(shell pwd) 83 | BIN_DIR ?= $(shell pwd) 84 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 85 | DOCKERFILE_PATH ?= ./Dockerfile 86 | DOCKERBUILD_CONTEXT ?= ./ 87 | DOCKER_REPO ?= prom 88 | 89 | DOCKER_ARCHS ?= amd64 90 | 91 | BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) 92 | PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) 93 | TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) 94 | 95 | SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) 96 | 97 | ifeq ($(GOHOSTARCH),amd64) 98 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) 99 | # Only supported on amd64 100 | test-flags := -race 101 | endif 102 | endif 103 | 104 | # This rule is used to forward a target like "build" to "common-build". This 105 | # allows a new "build" target to be defined in a Makefile which includes this 106 | # one and override "common-build" without override warnings. 107 | %: common-% ; 108 | 109 | .PHONY: common-all 110 | common-all: precheck style check_license lint yamllint unused build test 111 | 112 | .PHONY: common-style 113 | common-style: 114 | @echo ">> checking code style" 115 | @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ 116 | if [ -n "$${fmtRes}" ]; then \ 117 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 118 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 119 | exit 1; \ 120 | fi 121 | 122 | .PHONY: common-check_license 123 | common-check_license: 124 | @echo ">> checking license header" 125 | @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ 126 | awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ 127 | done); \ 128 | if [ -n "$${licRes}" ]; then \ 129 | echo "license header checking failed:"; echo "$${licRes}"; \ 130 | exit 1; \ 131 | fi 132 | 133 | .PHONY: common-deps 134 | common-deps: 135 | @echo ">> getting dependencies" 136 | $(GO) mod download 137 | 138 | .PHONY: update-go-deps 139 | update-go-deps: 140 | @echo ">> updating Go dependencies" 141 | @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ 142 | $(GO) get -d $$m; \ 143 | done 144 | $(GO) mod tidy 145 | 146 | .PHONY: common-test-short 147 | common-test-short: $(GOTEST_DIR) 148 | @echo ">> running short tests" 149 | $(GOTEST) -short $(GOOPTS) $(pkgs) 150 | 151 | .PHONY: common-test 152 | common-test: $(GOTEST_DIR) 153 | @echo ">> running all tests" 154 | $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) 155 | 156 | $(GOTEST_DIR): 157 | @mkdir -p $@ 158 | 159 | .PHONY: common-format 160 | common-format: $(GOLANGCI_LINT) 161 | @echo ">> formatting code" 162 | $(GO) fmt $(pkgs) 163 | ifdef GOLANGCI_LINT 164 | @echo ">> formatting code with golangci-lint" 165 | $(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS) 166 | endif 167 | 168 | .PHONY: common-vet 169 | common-vet: 170 | @echo ">> vetting code" 171 | $(GO) vet $(GOOPTS) $(pkgs) 172 | 173 | .PHONY: common-lint 174 | common-lint: $(GOLANGCI_LINT) 175 | ifdef GOLANGCI_LINT 176 | @echo ">> running golangci-lint" 177 | $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) 178 | endif 179 | 180 | .PHONY: common-lint-fix 181 | common-lint-fix: $(GOLANGCI_LINT) 182 | ifdef GOLANGCI_LINT 183 | @echo ">> running golangci-lint fix" 184 | $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) 185 | endif 186 | 187 | .PHONY: common-yamllint 188 | common-yamllint: 189 | @echo ">> running yamllint on all YAML files in the repository" 190 | ifeq (, $(shell command -v yamllint 2> /dev/null)) 191 | @echo "yamllint not installed so skipping" 192 | else 193 | yamllint . 194 | endif 195 | 196 | # For backward-compatibility. 197 | .PHONY: common-staticcheck 198 | common-staticcheck: lint 199 | 200 | .PHONY: common-unused 201 | common-unused: 202 | @echo ">> running check for unused/missing packages in go.mod" 203 | $(GO) mod tidy 204 | @git diff --exit-code -- go.sum go.mod 205 | 206 | .PHONY: common-build 207 | common-build: promu 208 | @echo ">> building binaries" 209 | $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) 210 | 211 | .PHONY: common-tarball 212 | common-tarball: promu 213 | @echo ">> building release tarball" 214 | $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 215 | 216 | .PHONY: common-docker-repo-name 217 | common-docker-repo-name: 218 | @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" 219 | 220 | .PHONY: common-docker $(BUILD_DOCKER_ARCHS) 221 | common-docker: $(BUILD_DOCKER_ARCHS) 222 | $(BUILD_DOCKER_ARCHS): common-docker-%: 223 | docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ 224 | -f $(DOCKERFILE_PATH) \ 225 | --build-arg ARCH="$*" \ 226 | --build-arg OS="linux" \ 227 | $(DOCKERBUILD_CONTEXT) 228 | 229 | .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) 230 | common-docker-publish: $(PUBLISH_DOCKER_ARCHS) 231 | $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: 232 | docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" 233 | 234 | DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) 235 | .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) 236 | common-docker-tag-latest: $(TAG_DOCKER_ARCHS) 237 | $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: 238 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" 239 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" 240 | 241 | .PHONY: common-docker-manifest 242 | common-docker-manifest: 243 | 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)) 244 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" 245 | 246 | .PHONY: promu 247 | promu: $(PROMU) 248 | 249 | $(PROMU): 250 | $(eval PROMU_TMP := $(shell mktemp -d)) 251 | curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) 252 | mkdir -p $(FIRST_GOPATH)/bin 253 | cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu 254 | rm -r $(PROMU_TMP) 255 | 256 | .PHONY: common-proto 257 | common-proto: 258 | @echo ">> generating code from proto files" 259 | @./scripts/genproto.sh 260 | 261 | ifdef GOLANGCI_LINT 262 | $(GOLANGCI_LINT): 263 | mkdir -p $(FIRST_GOPATH)/bin 264 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ 265 | | sed -e '/install -d/d' \ 266 | | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) 267 | endif 268 | 269 | .PHONY: precheck 270 | precheck:: 271 | 272 | define PRECHECK_COMMAND_template = 273 | precheck:: $(1)_precheck 274 | 275 | PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) 276 | .PHONY: $(1)_precheck 277 | $(1)_precheck: 278 | @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ 279 | echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ 280 | exit 1; \ 281 | fi 282 | endef 283 | 284 | govulncheck: install-govulncheck 285 | govulncheck ./... 286 | 287 | install-govulncheck: 288 | command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest 289 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Prometheus SNMP exporter 2 | Copyright 2016 The Prometheus Authors 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prometheus SNMP Exporter 2 | 3 | This exporter is the recommended way to expose SNMP data in a format which 4 | Prometheus can ingest. 5 | 6 | To simply get started, it's recommended to use the `if_mib` module with 7 | switches, access points, or routers using the `public_v2` auth module, 8 | which should be a read-only access community on the target device. 9 | 10 | Note, community strings in SNMP are not considered secrets, as they are sent 11 | unencrypted in SNMP v1 and v2c. For secure access, SNMP v3 is required. 12 | 13 | # Concepts 14 | 15 | While SNMP uses a hierarchical data structure and Prometheus uses an 16 | n-dimensional matrix, the two systems map perfectly, and without the need 17 | to walk through data by hand. `snmp_exporter` maps the data for you. 18 | 19 | ## Prometheus 20 | 21 | Prometheus is able to map SNMP index instances to labels. For example, the `ifEntry` specifies an INDEX of `ifIndex`. This becomes the `ifIndex` label in Prometheus. 22 | 23 | If an SNMP entry has multiple index values, each value is mapped to a separate Prometheus label. 24 | 25 | ## SNMP 26 | 27 | SNMP is structured in OID trees, described by MIBs. OID subtrees have the same 28 | order across different locations in the tree. The order under 29 | `1.3.6.1.2.1.2.2.1.1` (`ifIndex`) is the same as in `1.3.6.1.2.1.2.2.1.2` 30 | (`ifDescr`), `1.3.6.1.2.1.31.1.1.1.10` (`ifHCOutOctets`), etc. The numbers are 31 | OIDs, the names in parentheses are the names from a MIB, in this case 32 | [IF-MIB](http://www.oidview.com/mibs/0/IF-MIB.html). 33 | 34 | ## Mapping 35 | 36 | Given a device with an interface at number 2, a partial `snmpwalk` return looks 37 | like: 38 | 39 | ``` 40 | 1.3.6.1.2.1.2.2.1.1.2 = INTEGER: 2 # ifIndex for '2' is literally just '2' 41 | 1.3.6.1.2.1.2.2.1.2.2 = STRING: "eth0" # ifDescr 42 | 1.3.6.1.2.1.31.1.1.1.1.2 = STRING: "eth0" # IfName 43 | 1.3.6.1.2.1.31.1.1.1.10.2 = INTEGER: 1000 # ifHCOutOctets, 1000 bytes 44 | 1.3.6.1.2.1.31.1.1.1.18.2 = STRING: "" # ifAlias 45 | ``` 46 | 47 | `snmp_exporter` combines all of this data into: 48 | 49 | ``` 50 | ifHCOutOctets{ifAlias="",ifDescr="eth0",ifIndex="2",ifName="eth0"} 1000 51 | ``` 52 | 53 | # Scaling 54 | 55 | A single instance of `snmp_exporter` can be run for thousands of devices. 56 | 57 | # Usage 58 | 59 | ## Installation 60 | 61 | Binaries can be downloaded from the [Github 62 | releases](https://github.com/prometheus/snmp_exporter/releases) page and need no 63 | special installation. 64 | 65 | We also provide a sample [systemd unit file](examples/systemd/snmp_exporter.service). 66 | 67 | ## Running 68 | 69 | Start `snmp_exporter` as a daemon or from CLI: 70 | 71 | ```sh 72 | ./snmp_exporter 73 | ``` 74 | 75 | Visit where `192.0.0.8` is the IP or 76 | FQDN of the SNMP device to get metrics from. Note that this will use the default transport (`udp`), 77 | default port (`161`), default auth (`public_v2`) and default module (`if_mib`). The auth and module 78 | must be defined in the `snmp.yml` file. 79 | 80 | For example, if you have an auth named `my_secure_v3` for walking `ddwrt`, the URL would look like 81 | . 82 | 83 | To configure a different transport and/or port, use the syntax `[transport://]host[:port]`. 84 | 85 | For example, to scrape a device using `tcp` on port `1161`, the URL would look like 86 | . 87 | 88 | Note that [URL encoding](https://en.wikipedia.org/wiki/URL_encoding) should be used for `target` due 89 | to the `:` and `/` characters. Prometheus encodes query parameters automatically and manual encoding 90 | is not necessary within the Prometheus configuration file. 91 | 92 | Metrics concerning the operation of the exporter itself are available at the 93 | endpoint . 94 | 95 | It is possible to supply an optional `snmp_context` parameter in the URL, like this: 96 | 97 | The `snmp_context` parameter in the URL would override the `context_name` parameter in the `snmp.yml` file. 98 | 99 | ## Multi-Module Handling 100 | The multi-module functionality allows you to specify multiple modules, enabling the retrieval of information from several modules in a single scrape. 101 | The concurrency can be specified using the snmp-exporter option `--snmp.module-concurrency` (the default is 1). 102 | 103 | Note: This implementation does not perform any de-duplication of walks between different modules. 104 | 105 | There are two ways to specify multiple modules. You can either separate them with a comma or define multiple params_module. 106 | The URLs would look like this: 107 | 108 | For comma separation: 109 | ``` 110 | http://localhost:9116/snmp?module=if_mib,arista_sw&target=192.0.0.8 111 | ``` 112 | 113 | For multiple params_module: 114 | ``` 115 | http://localhost:9116/snmp?module=if_mib&module=arista_sw&target=192.0.0.8 116 | ``` 117 | 118 | Prometheus Example: 119 | ```YAML 120 | 121 | - job_name: 'my' 122 | params: 123 | module: 124 | - if_mib 125 | - synology 126 | - ucd_la_table 127 | ``` 128 | 129 | ## Configuration 130 | 131 | The default configuration file name is `snmp.yml` and should not be edited 132 | by hand. If you need to change it, see 133 | [Generating configuration](#generating-configuration). 134 | 135 | The default `snmp.yml` file covers a variety of common hardware walking them 136 | using SNMP v2 GETBULK. 137 | 138 | The `--config.file` parameter can be used multiple times to load more than one file. 139 | It also supports [glob filename matching](https://pkg.go.dev/path/filepath#Glob), e.g. `snmp*.yml`. 140 | 141 | The `--config.expand-environment-variables` parameter allows passing environment variables into some fields of the configuration file. The `username`, `password` & `priv_password` fields in the auths section are supported. Defaults to disabled. 142 | 143 | Duplicate `module` or `auth` entries are treated as invalid and can not be loaded. 144 | 145 | ## Prometheus Configuration 146 | 147 | The URL params `target`, `auth`, and `module` can be controlled through relabelling. 148 | 149 | Example config: 150 | ```YAML 151 | scrape_configs: 152 | - job_name: 'snmp' 153 | static_configs: 154 | - targets: 155 | - 192.168.1.2 # SNMP device. 156 | - switch.local # SNMP device. 157 | - tcp://192.168.1.3:1161 # SNMP device using TCP transport and custom port. 158 | metrics_path: /snmp 159 | params: 160 | auth: [public_v2] 161 | module: [if_mib] 162 | relabel_configs: 163 | - source_labels: [__address__] 164 | target_label: __param_target 165 | - source_labels: [__param_target] 166 | target_label: instance 167 | - target_label: __address__ 168 | replacement: 127.0.0.1:9116 # The SNMP exporter's real hostname:port. 169 | 170 | # Global exporter-level metrics 171 | - job_name: 'snmp_exporter' 172 | static_configs: 173 | - targets: ['localhost:9116'] 174 | ``` 175 | 176 | You could pass `username`, `password` & `priv_password` via environment variables of your choice in below format. 177 | If the variables exist in the environment, they are resolved on the fly otherwise the string in the config file is passed as-is. 178 | 179 | This requires the `--config.expand-environment-variables` flag be set. 180 | 181 | ```YAML 182 | auths: 183 | example_with_envs: 184 | community: mysecret 185 | security_level: SomethingReadOnly 186 | username: ${ARISTA_USERNAME} 187 | password: ${ARISTA_PASSWORD} 188 | auth_protocol: SHA256 189 | priv_protocol: AES 190 | priv_password: ${ARISTA_PRIV_PASSWORD} 191 | ``` 192 | 193 | Similarly to [blackbox_exporter](https://github.com/prometheus/blackbox_exporter), 194 | `snmp_exporter` is meant to run on a few central machines and can be thought of 195 | like a "Prometheus proxy". 196 | 197 | ### TLS and basic authentication 198 | 199 | The SNMP Exporter supports TLS and basic authentication. This enables better 200 | control of the various HTTP endpoints. 201 | 202 | To use TLS and/or basic authentication, you need to pass a configuration file 203 | using the `--web.config.file` parameter. The format of the file is described 204 | [in the exporter-toolkit repository](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md). 205 | 206 | Note that the TLS and basic authentication settings affect all HTTP endpoints: 207 | /metrics for scraping, /snmp for scraping SNMP devices, and the web UI. 208 | 209 | ### Generating configuration 210 | 211 | Most use cases should be covered by our [default configuration](snmp.yml). 212 | If you need to generate your own configuration from MIBs, you can use the 213 | [generator](generator/). 214 | 215 | Use the generator if you need to customize which objects are walked or use 216 | non-public MIBs. 217 | 218 | ## Large counter value handling 219 | 220 | In order to provide accurate counters for large Counter64 values, the exporter 221 | will automatically wrap the value every 2^53 to avoid 64-bit float rounding. 222 | Prometheus handles this gracefully for you and you will not notice any negative 223 | effects. 224 | 225 | If you need to disable this feature for non-Prometheus systems, use the 226 | command line flag `--no-snmp.wrap-large-counters`. 227 | 228 | # Once you have it running 229 | 230 | It can be opaque to get started with all this, but in our own experience, 231 | snmp_exporter is honestly the best way to interact with SNMP. To make it 232 | easier for others, please consider contributing back your configurations to 233 | us. 234 | `snmp.yml` config should be accompanied by generator config. 235 | For your dashboard, alerts, and recording rules, please consider 236 | contributing them to . 237 | -------------------------------------------------------------------------------- /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 | 0.29.0 2 | -------------------------------------------------------------------------------- /auth-split-migration.md: -------------------------------------------------------------------------------- 1 | # Module and Auth Split Migration 2 | 3 | Starting from `snmp_exporter` [release v0.23.0](https://github.com/prometheus/snmp_exporter/releases/tag/v0.23.0) the configuration file format for the `snmp_exporter` has been changed. Configuration files for [release v0.22.0](https://github.com/prometheus/snmp_exporter/releases/tag/v0.22.0) and before will not work. The configuration was split from a flat list of modules to separate metric walking/mapping modules and authentication configurations. 4 | 5 | This change necessitates migration of the generator config and `snmp_exporter` config to the new format. 6 | 7 | The complete `generator` format is [documented in generator/README.md#file-format](generator/README.md#file-format) 8 | 9 | The complete `snmp_exporter` format is [documented in /generator/FORMAT.md](/generator/FORMAT.md). 10 | 11 | See the main [README](/README#Configuration) for the Prometheus configuration examples. 12 | 13 | ## Examples 14 | 15 | A generator containing the following config: 16 | 17 | ```yaml 18 | modules: 19 | sys_uptime: 20 | version: 2 21 | walk: 22 | - sysUpTime 23 | auth: 24 | community: public 25 | ``` 26 | 27 | Would now become: 28 | 29 | ```yaml 30 | auths: 31 | public_v2: 32 | community: public 33 | version: 2 34 | modules: 35 | sys_uptime: 36 | walk: 37 | - sysUpTime 38 | ``` 39 | 40 | The newly generated `snmp_exporter` config would be: 41 | 42 | ```yaml 43 | # WARNING: This file was auto-generated using snmp_exporter generator, manual changes will be lost. 44 | auths: 45 | public_v2: 46 | community: public 47 | security_level: noAuthNoPriv 48 | auth_protocol: MD5 49 | priv_protocol: DES 50 | version: 2 51 | modules: 52 | if_mib: 53 | get: 54 | - 1.3.6.1.2.1.1.3.0 55 | metrics: 56 | - name: sysUpTime 57 | oid: 1.3.6.1.2.1.1.3 58 | type: gauge 59 | help: The time (in hundredths of a second) since the network management portion 60 | of the system was last re-initialized. - 1.3.6.1.2.1.1.3 61 | ``` 62 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 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 | package config 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | "regexp" 22 | "time" 23 | 24 | "github.com/gosnmp/gosnmp" 25 | "gopkg.in/yaml.v2" 26 | ) 27 | 28 | func LoadFile(paths []string, expandEnvVars bool) (*Config, error) { 29 | cfg := &Config{} 30 | for _, p := range paths { 31 | files, err := filepath.Glob(p) 32 | if err != nil { 33 | return nil, err 34 | } 35 | for _, f := range files { 36 | content, err := os.ReadFile(f) 37 | if err != nil { 38 | return nil, err 39 | } 40 | err = yaml.UnmarshalStrict(content, cfg) 41 | if err != nil { 42 | return nil, err 43 | } 44 | } 45 | } 46 | 47 | if expandEnvVars { 48 | var err error 49 | for i, auth := range cfg.Auths { 50 | if auth.Username != "" { 51 | cfg.Auths[i].Username, err = substituteEnvVariables(auth.Username) 52 | if err != nil { 53 | return nil, err 54 | } 55 | } 56 | if auth.Password != "" { 57 | password, err := substituteEnvVariables(string(auth.Password)) 58 | if err != nil { 59 | return nil, err 60 | } 61 | cfg.Auths[i].Password.Set(password) 62 | } 63 | if auth.PrivPassword != "" { 64 | privPassword, err := substituteEnvVariables(string(auth.PrivPassword)) 65 | if err != nil { 66 | return nil, err 67 | } 68 | cfg.Auths[i].PrivPassword.Set(privPassword) 69 | } 70 | } 71 | } 72 | 73 | return cfg, nil 74 | } 75 | 76 | var ( 77 | defaultRetries = 3 78 | 79 | DefaultAuth = Auth{ 80 | Community: "public", 81 | SecurityLevel: "noAuthNoPriv", 82 | AuthProtocol: "MD5", 83 | PrivProtocol: "DES", 84 | Version: 2, 85 | } 86 | DefaultWalkParams = WalkParams{ 87 | MaxRepetitions: 25, 88 | Retries: &defaultRetries, 89 | Timeout: time.Second * 5, 90 | UseUnconnectedUDPSocket: false, 91 | AllowNonIncreasingOIDs: false, 92 | } 93 | DefaultModule = Module{ 94 | WalkParams: DefaultWalkParams, 95 | } 96 | DefaultRegexpExtract = RegexpExtract{ 97 | Value: "$1", 98 | } 99 | ) 100 | 101 | // Config for the snmp_exporter. 102 | type Config struct { 103 | Auths map[string]*Auth `yaml:"auths,omitempty"` 104 | Modules map[string]*Module `yaml:"modules,omitempty"` 105 | Version int `yaml:"version,omitempty"` 106 | } 107 | 108 | type WalkParams struct { 109 | MaxRepetitions uint32 `yaml:"max_repetitions,omitempty"` 110 | Retries *int `yaml:"retries,omitempty"` 111 | Timeout time.Duration `yaml:"timeout,omitempty"` 112 | UseUnconnectedUDPSocket bool `yaml:"use_unconnected_udp_socket,omitempty"` 113 | AllowNonIncreasingOIDs bool `yaml:"allow_nonincreasing_oids,omitempty"` 114 | } 115 | 116 | type Module struct { 117 | // A list of OIDs. 118 | Walk []string `yaml:"walk,omitempty"` 119 | Get []string `yaml:"get,omitempty"` 120 | Metrics []*Metric `yaml:"metrics"` 121 | WalkParams WalkParams `yaml:",inline"` 122 | Filters []DynamicFilter `yaml:"filters,omitempty"` 123 | } 124 | 125 | func (c *Module) UnmarshalYAML(unmarshal func(interface{}) error) error { 126 | *c = DefaultModule 127 | type plain Module 128 | return unmarshal((*plain)(c)) 129 | } 130 | 131 | // ConfigureSNMP sets the various version and auth settings. 132 | func (c Auth) ConfigureSNMP(g *gosnmp.GoSNMP, snmpContext string) { 133 | switch c.Version { 134 | case 1: 135 | g.Version = gosnmp.Version1 136 | case 2: 137 | g.Version = gosnmp.Version2c 138 | case 3: 139 | g.Version = gosnmp.Version3 140 | } 141 | g.Community = string(c.Community) 142 | 143 | if snmpContext == "" { 144 | g.ContextName = c.ContextName 145 | } else { 146 | g.ContextName = snmpContext 147 | } 148 | 149 | // v3 security settings. 150 | g.SecurityModel = gosnmp.UserSecurityModel 151 | usm := &gosnmp.UsmSecurityParameters{ 152 | UserName: c.Username, 153 | } 154 | auth, priv := false, false 155 | switch c.SecurityLevel { 156 | case "noAuthNoPriv": 157 | g.MsgFlags = gosnmp.NoAuthNoPriv 158 | case "authNoPriv": 159 | g.MsgFlags = gosnmp.AuthNoPriv 160 | auth = true 161 | case "authPriv": 162 | g.MsgFlags = gosnmp.AuthPriv 163 | auth = true 164 | priv = true 165 | } 166 | if auth { 167 | usm.AuthenticationPassphrase = string(c.Password) 168 | switch c.AuthProtocol { 169 | case "SHA": 170 | usm.AuthenticationProtocol = gosnmp.SHA 171 | case "SHA224": 172 | usm.AuthenticationProtocol = gosnmp.SHA224 173 | case "SHA256": 174 | usm.AuthenticationProtocol = gosnmp.SHA256 175 | case "SHA384": 176 | usm.AuthenticationProtocol = gosnmp.SHA384 177 | case "SHA512": 178 | usm.AuthenticationProtocol = gosnmp.SHA512 179 | case "MD5": 180 | usm.AuthenticationProtocol = gosnmp.MD5 181 | } 182 | } 183 | if priv { 184 | usm.PrivacyPassphrase = string(c.PrivPassword) 185 | switch c.PrivProtocol { 186 | case "DES": 187 | usm.PrivacyProtocol = gosnmp.DES 188 | case "AES": 189 | usm.PrivacyProtocol = gosnmp.AES 190 | case "AES192": 191 | usm.PrivacyProtocol = gosnmp.AES192 192 | case "AES192C": 193 | usm.PrivacyProtocol = gosnmp.AES192C 194 | case "AES256": 195 | usm.PrivacyProtocol = gosnmp.AES256 196 | case "AES256C": 197 | usm.PrivacyProtocol = gosnmp.AES256C 198 | } 199 | } 200 | g.SecurityParameters = usm 201 | } 202 | 203 | type Filters struct { 204 | Static []StaticFilter `yaml:"static,omitempty"` 205 | Dynamic []DynamicFilter `yaml:"dynamic,omitempty"` 206 | } 207 | 208 | type StaticFilter struct { 209 | Targets []string `yaml:"targets,omitempty"` 210 | Indices []string `yaml:"indices,omitempty"` 211 | } 212 | type DynamicFilter struct { 213 | Oid string `yaml:"oid"` 214 | Targets []string `yaml:"targets,omitempty"` 215 | Values []string `yaml:"values,omitempty"` 216 | } 217 | 218 | type Metric struct { 219 | Name string `yaml:"name"` 220 | Oid string `yaml:"oid"` 221 | Type string `yaml:"type"` 222 | Help string `yaml:"help"` 223 | Indexes []*Index `yaml:"indexes,omitempty"` 224 | Lookups []*Lookup `yaml:"lookups,omitempty"` 225 | RegexpExtracts map[string][]RegexpExtract `yaml:"regex_extracts,omitempty"` 226 | DateTimePattern string `yaml:"datetime_pattern,omitempty"` 227 | EnumValues map[int]string `yaml:"enum_values,omitempty"` 228 | Offset float64 `yaml:"offset,omitempty"` 229 | Scale float64 `yaml:"scale,omitempty"` 230 | } 231 | 232 | type Index struct { 233 | Labelname string `yaml:"labelname"` 234 | Type string `yaml:"type"` 235 | FixedSize int `yaml:"fixed_size,omitempty"` 236 | Implied bool `yaml:"implied,omitempty"` 237 | EnumValues map[int]string `yaml:"enum_values,omitempty"` 238 | } 239 | 240 | type Lookup struct { 241 | Labels []string `yaml:"labels"` 242 | Labelname string `yaml:"labelname"` 243 | Oid string `yaml:"oid,omitempty"` 244 | Type string `yaml:"type,omitempty"` 245 | } 246 | 247 | // Secret is a string that must not be revealed on marshaling. 248 | type Secret string 249 | 250 | func (s *Secret) Set(value string) { 251 | *s = Secret(value) 252 | } 253 | 254 | // Hack for creating snmp.yml with the secret. 255 | var ( 256 | DoNotHideSecrets = false 257 | ) 258 | 259 | // MarshalYAML implements the yaml.Marshaler interface. 260 | func (s Secret) MarshalYAML() (interface{}, error) { 261 | if DoNotHideSecrets { 262 | return string(s), nil 263 | } 264 | if s != "" { 265 | return "", nil 266 | } 267 | return nil, nil 268 | } 269 | 270 | type Auth struct { 271 | Community Secret `yaml:"community,omitempty"` 272 | SecurityLevel string `yaml:"security_level,omitempty"` 273 | Username string `yaml:"username,omitempty"` 274 | Password Secret `yaml:"password,omitempty"` 275 | AuthProtocol string `yaml:"auth_protocol,omitempty"` 276 | PrivProtocol string `yaml:"priv_protocol,omitempty"` 277 | PrivPassword Secret `yaml:"priv_password,omitempty"` 278 | ContextName string `yaml:"context_name,omitempty"` 279 | Version int `yaml:"version,omitempty"` 280 | } 281 | 282 | func (c *Auth) UnmarshalYAML(unmarshal func(interface{}) error) error { 283 | *c = DefaultAuth 284 | type plain Auth 285 | if err := unmarshal((*plain)(c)); err != nil { 286 | return err 287 | } 288 | 289 | if c.Version < 1 || c.Version > 3 { 290 | return fmt.Errorf("SNMP version must be 1, 2 or 3. Got: %d", c.Version) 291 | } 292 | if c.Version == 3 { 293 | switch c.SecurityLevel { 294 | case "authPriv": 295 | if c.PrivPassword == "" { 296 | return fmt.Errorf("priv password is missing, required for SNMPv3 with priv") 297 | } 298 | if c.PrivProtocol != "DES" && c.PrivProtocol != "AES" && c.PrivProtocol != "AES192" && c.PrivProtocol != "AES192C" && c.PrivProtocol != "AES256" && c.PrivProtocol != "AES256C" { 299 | return fmt.Errorf("priv protocol must be DES or AES") 300 | } 301 | fallthrough 302 | case "authNoPriv": 303 | if c.Password == "" { 304 | return fmt.Errorf("auth password is missing, required for SNMPv3 with auth") 305 | } 306 | if c.AuthProtocol != "MD5" && c.AuthProtocol != "SHA" && c.AuthProtocol != "SHA224" && c.AuthProtocol != "SHA256" && c.AuthProtocol != "SHA384" && c.AuthProtocol != "SHA512" { 307 | return fmt.Errorf("auth protocol must be SHA or MD5") 308 | } 309 | fallthrough 310 | case "noAuthNoPriv": 311 | if c.Username == "" { 312 | return fmt.Errorf("auth username is missing, required for SNMPv3") 313 | } 314 | default: 315 | return fmt.Errorf("security level must be one of authPriv, authNoPriv or noAuthNoPriv") 316 | } 317 | } 318 | return nil 319 | } 320 | 321 | type RegexpExtract struct { 322 | Value string `yaml:"value"` 323 | Regex Regexp `yaml:"regex"` 324 | } 325 | 326 | func (c *RegexpExtract) UnmarshalYAML(unmarshal func(interface{}) error) error { 327 | *c = DefaultRegexpExtract 328 | type plain RegexpExtract 329 | return unmarshal((*plain)(c)) 330 | } 331 | 332 | // Regexp encapsulates a regexp.Regexp and makes it YAML marshalable. 333 | type Regexp struct { 334 | *regexp.Regexp 335 | } 336 | 337 | // MarshalYAML implements the yaml.Marshaler interface. 338 | func (re Regexp) MarshalYAML() (interface{}, error) { 339 | if re.Regexp != nil { 340 | return re.String(), nil 341 | } 342 | return nil, nil 343 | } 344 | 345 | // UnmarshalYAML implements the yaml.Unmarshaler interface. 346 | func (re *Regexp) UnmarshalYAML(unmarshal func(interface{}) error) error { 347 | var s string 348 | if err := unmarshal(&s); err != nil { 349 | return err 350 | } 351 | regex, err := regexp.Compile("^(?:" + s + ")$") 352 | if err != nil { 353 | return err 354 | } 355 | re.Regexp = regex 356 | return nil 357 | } 358 | 359 | func substituteEnvVariables(value string) (string, error) { 360 | result := os.Expand(value, func(s string) string { 361 | return os.Getenv(s) 362 | }) 363 | if result == "" { 364 | return "", errors.New(value + " environment variable not found") 365 | } 366 | return result, nil 367 | } 368 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 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 | package main 15 | 16 | import ( 17 | "strings" 18 | "testing" 19 | 20 | yaml "gopkg.in/yaml.v2" 21 | ) 22 | 23 | func TestHideConfigSecrets(t *testing.T) { 24 | sc := &SafeConfig{} 25 | err := sc.ReloadConfig([]string{"testdata/snmp-auth.yml"}, false) 26 | if err != nil { 27 | t.Errorf("Error loading config %v: %v", "testdata/snmp-auth.yml", err) 28 | } 29 | 30 | // String method must not reveal authentication credentials. 31 | sc.RLock() 32 | c, err := yaml.Marshal(sc.C) 33 | sc.RUnlock() 34 | if err != nil { 35 | t.Errorf("Error marshaling config: %v", err) 36 | } 37 | if strings.Contains(string(c), "mysecret") { 38 | t.Fatal("config's String method reveals authentication credentials.") 39 | } 40 | } 41 | 42 | func TestLoadConfigWithOverrides(t *testing.T) { 43 | sc := &SafeConfig{} 44 | err := sc.ReloadConfig([]string{"testdata/snmp-with-overrides.yml"}, false) 45 | if err != nil { 46 | t.Errorf("Error loading config %v: %v", "testdata/snmp-with-overrides.yml", err) 47 | } 48 | sc.RLock() 49 | _, err = yaml.Marshal(sc.C) 50 | sc.RUnlock() 51 | if err != nil { 52 | t.Errorf("Error marshaling config: %v", err) 53 | } 54 | } 55 | 56 | func TestLoadMultipleConfigs(t *testing.T) { 57 | sc := &SafeConfig{} 58 | configs := []string{"testdata/snmp-auth.yml", "testdata/snmp-with-overrides.yml"} 59 | err := sc.ReloadConfig(configs, false) 60 | if err != nil { 61 | t.Errorf("Error loading configs %v: %v", configs, err) 62 | } 63 | sc.RLock() 64 | _, err = yaml.Marshal(sc.C) 65 | sc.RUnlock() 66 | if err != nil { 67 | t.Errorf("Error marshaling config: %v", err) 68 | } 69 | } 70 | 71 | // When all environment variables are present 72 | func TestEnvSecrets(t *testing.T) { 73 | t.Setenv("ENV_USERNAME", "snmp_username") 74 | t.Setenv("ENV_PASSWORD", "snmp_password") 75 | t.Setenv("ENV_PRIV_PASSWORD", "snmp_priv_password") 76 | 77 | sc := &SafeConfig{} 78 | err := sc.ReloadConfig([]string{"testdata/snmp-auth-envvars.yml"}, true) 79 | if err != nil { 80 | t.Errorf("Error loading config %v: %v", "testdata/snmp-auth-envvars.yml", err) 81 | } 82 | 83 | // String method must not reveal authentication credentials. 84 | sc.RLock() 85 | c, err := yaml.Marshal(sc.C) 86 | sc.RUnlock() 87 | if err != nil { 88 | t.Errorf("Error marshaling config: %v", err) 89 | } 90 | 91 | if strings.Contains(string(c), "mysecret") { 92 | t.Fatal("config's String method reveals authentication credentials.") 93 | } 94 | 95 | // we check whether vars we set are resolved correctly in config 96 | for i := range sc.C.Auths { 97 | if sc.C.Auths[i].Username != "snmp_username" || sc.C.Auths[i].Password != "snmp_password" || sc.C.Auths[i].PrivPassword != "snmp_priv_password" { 98 | t.Fatal("failed to resolve secrets from env vars") 99 | } 100 | } 101 | } 102 | 103 | // When environment variable(s) are absent 104 | func TestEnvSecretsMissing(t *testing.T) { 105 | t.Setenv("ENV_PASSWORD", "snmp_password") 106 | t.Setenv("ENV_PRIV_PASSWORD", "snmp_priv_password") 107 | 108 | sc := &SafeConfig{} 109 | err := sc.ReloadConfig([]string{"testdata/snmp-auth-envvars.yml"}, true) 110 | if err != nil { 111 | // we check the error message pattern to determine the error 112 | if strings.Contains(err.Error(), "environment variable not found") { 113 | t.Logf("Error loading config as env var is not set/missing %v: %v", "testdata/snmp-auth-envvars.yml", err) 114 | } else { 115 | t.Errorf("Error loading config %v: %v", "testdata/snmp-auth-envvars.yml", err) 116 | } 117 | } 118 | } 119 | 120 | // When SNMPv2 was specified without credentials 121 | func TestEnvSecretsNotSpecified(t *testing.T) { 122 | sc := &SafeConfig{} 123 | err := sc.ReloadConfig([]string{"testdata/snmp-auth-v2nocreds.yml"}, true) 124 | if err != nil { 125 | t.Errorf("Error loading config %v: %v", "testdata/snmp-auth-v2nocreds.yml", err) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /examples/systemd/snmp_exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SNMP Exporter 3 | After=network-online.target 4 | 5 | # This assumes you are running snmp_exporter under the user "prometheus" 6 | 7 | [Service] 8 | User=prometheus 9 | Restart=on-failure 10 | ExecStart=/home/prometheus/snmp_exporter/snmp_exporter --config.file=/home/prometheus/snmp_exporter/snmp.yml 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /generator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:bookworm AS builder 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y --no-install-recommends libsnmp-dev 5 | 6 | ARG REPO_TAG=main 7 | RUN go install github.com/prometheus/snmp_exporter/generator@"$REPO_TAG" 8 | 9 | FROM debian:bookworm-slim 10 | 11 | WORKDIR "/opt" 12 | 13 | ENTRYPOINT ["/bin/generator"] 14 | 15 | ENV MIBDIRS=mibs 16 | 17 | CMD ["generate"] 18 | 19 | RUN apt-get update \ 20 | && apt-get install -y --no-install-recommends libsnmp40 \ 21 | && rm -rf /var/lib/apt/lists/* 22 | 23 | COPY --from=builder /go/bin/generator /bin/generator 24 | -------------------------------------------------------------------------------- /generator/Dockerfile-local: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y libsnmp-dev p7zip-full 5 | 6 | COPY ./generator /bin/generator 7 | 8 | WORKDIR "/opt" 9 | 10 | ENTRYPOINT ["/bin/generator"] 11 | 12 | ENV MIBDIRS mibs 13 | 14 | CMD ["generate"] 15 | -------------------------------------------------------------------------------- /generator/FORMAT.md: -------------------------------------------------------------------------------- 1 | # Exporter file format 2 | 3 | This is generated by the generator, so only those doing development should 4 | have to care about how this works. 5 | 6 | ``` 7 | auths: 8 | public_v2: 9 | version: 2 10 | # There's various auth/version options here too. See the main README. 11 | community: public 12 | modules: 13 | module_name: 14 | walk: 15 | # List of OID subtrees to walk. 16 | - "IF-MIB::interfaces" # 1.3.6.1.2.1.2 from IF-MIB 17 | - ifXTable # 1.3.6.1.2.1.31.1.1 from IF-MIB 18 | get: 19 | # List of OIDs to get directly. 20 | - 1.3.6.1.2.1.1.3 21 | metrics: # List of metrics to extract. 22 | # A simple metric with no labels. 23 | - name: sysUpTime 24 | oid: 1.3.6.1.2.1.1.3 25 | type: gauge 26 | # The HELP text included with the scrape metrics. 27 | help: The value of sysUpTime on the most recent occasion at which any one or 28 | more of this entry's counters suffered a discontinuity - 1.3.6.1.4.1.30065.3.1.1.1.1.46 29 | # See README.md type override for a list of valid types 30 | # Non-numeric types are represented as a gauge with value 1, and the rendered value 31 | # as a label value on that gauge. 32 | 33 | # A metric that's part of a table, and thus has labels. 34 | - name: ifMtu 35 | oid: 1.3.6.1.2.1.2.2.1.4 36 | type: gauge 37 | # A list of the table indexes and their types. All indexes become labels. 38 | indexes: 39 | - labelname: ifIndex 40 | type: gauge 41 | - labelname: someString 42 | type: OctetString 43 | fixed_size: 8 # Only possible for OctetString/DisplayString types. 44 | # If only one length is possible this is it. Otherwise 45 | # this will be 0 or missing. 46 | - labelname: someOtherString 47 | type: OctetString 48 | implied: true # Only possible for OctetString/DisplayString types. 49 | # Must be the last index. See RFC2578 section 7.7. 50 | - name: ifSpeed 51 | oid: 1.3.6.1.2.1.2.2.1.5 52 | type: gauge 53 | indexes: 54 | - labelname: ifDescr 55 | type: gauge 56 | # Lookups take original indexes, look them up in another part of the 57 | # oid tree and overwrite the given output label. 58 | lookups: 59 | - labels: [ifDescr] # Input label name(s). Empty means delete the output label. 60 | oid: 1.3.6.1.2.1.2.2.1.2 # OID to look under. 61 | labelname: ifDescr # Output label name. 62 | type: OctetString # Type of output object. 63 | # Creates new metrics based on the regex and the metric value. 64 | regex_extracts: 65 | Temp: # A new metric will be created appending this to the metricName to become metricNameTemp. 66 | - regex: '(.*)' # Regex to extract a value from the returned SNMP walks's value. 67 | value: '$1' # Parsed as float64, defaults to $1. 68 | offset: 0.0 # Adds the value to the sample. Applied after scale. 69 | scale: 0.125 # Scale the sample by this value, for example bits to bytes. 70 | enum_values: # Enum for this metric. Only used with the enum types. 71 | 0: true 72 | 1: false 73 | ``` 74 | -------------------------------------------------------------------------------- /generator/Makefile: -------------------------------------------------------------------------------- 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 | MIBDIR := mibs 15 | MIB_PATH := 'mibs' 16 | 17 | CURL_OPTS ?= -L -sS --retry 3 --retry-delay 3 --fail 18 | CURL_USER_AGENT ?= -H "User-Agent: snmp_exporter generator" 19 | 20 | REPO_TAG ?= $(shell git rev-parse --abbrev-ref HEAD) 21 | 22 | DOCKER_IMAGE_NAME ?= snmp-generator 23 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(REPO_TAG)) 24 | DOCKER_REPO ?= prom 25 | 26 | SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) 27 | 28 | SELINUX_ENABLED := $(shell cat /sys/fs/selinux/enforce 2> /dev/null || echo 0) 29 | 30 | ifeq ($(SELINUX_ENABLED),1) 31 | DOCKER_VOL_OPTS ?= :z 32 | endif 33 | 34 | APC_URL := https://download.schneider-electric.com/files?p_Doc_Ref=APC_POWERNETMIB_EN&p_enDocType=Firmware 35 | ARISTA_URL := https://www.arista.com/assets/data/docs/MIBS 36 | CISCO_URL := https://raw.githubusercontent.com/cisco/cisco-mibs/f55dc443daff58dfc86a764047ded2248bb94e12/v2 37 | DELL_URL := https://dl.dell.com/FOLDER11196144M/1/Dell-OM-MIBS-11010_A00.zip 38 | DLINK_URL := https://ftp.dlink.de/des/des-3200-18/driver_software/DES-3200-18_mib_revC_4-04_all_en_20130603.zip 39 | ELTEX_MES_URL := https://api.prod.eltex-co.ru/storage/upload_center/files/33/mibs_10.3.6.13.zip 40 | DELL_NETWORK_URL := https://supportkb.dell.com/attachment/ka02R000000I7TFQA0/Current_MIBs_pkb_en_US_1.zip 41 | HPE_URL := https://downloads.hpe.com/pub/softlib2/software1/pubsw-linux/p1580676047/v229101/upd11.85mib.tar.gz 42 | IANA_CHARSET_URL := https://www.iana.org/assignments/ianacharset-mib/ianacharset-mib 43 | IANA_IFTYPE_URL := https://www.iana.org/assignments/ianaiftype-mib/ianaiftype-mib 44 | IANA_PRINTER_URL := https://www.iana.org/assignments/ianaprinter-mib/ianaprinter-mib 45 | FIELDSERVER_URL := https://media.msanet.com/NA/USA/SMC/SoftwareDownloads/FS-8704-26%20SNMP%20Standard%20MIB%20File.zip 46 | KEEPALIVED_URL := https://raw.githubusercontent.com/acassen/keepalived/v2.2.8/doc/KEEPALIVED-MIB.txt 47 | VRRP_URL := https://raw.githubusercontent.com/acassen/keepalived/v2.2.8/doc/VRRP-MIB.txt 48 | VRRPV3_URL := https://raw.githubusercontent.com/acassen/keepalived/v2.2.8/doc/VRRPv3-MIB.txt 49 | KEMP_LM_URL := https://kemptechnologies.com/files/packages/current/LM_mibs.zip 50 | MIKROTIK_URL := https://download.mikrotik.com/routeros/7.18.2/mikrotik.mib 51 | NEC_URL := https://jpn.nec.com/univerge/ix/Manual/MIB 52 | NET_SNMP_URL := https://raw.githubusercontent.com/net-snmp/net-snmp/v5.9/mibs 53 | PALOALTO_URL := https://docs.paloaltonetworks.com/content/dam/techdocs/en_US/zip/snmp-mib/pan-10-0-snmp-mib-modules.zip 54 | PRINTER_URL := https://ftp.pwg.org/pub/pwg/pmp/mibs/rfc3805b.mib 55 | SERVERTECH_URL := 'https://cdn10.servertech.com/assets/documents/documents/817/original/Sentry3.mib' 56 | SERVERTECH4_URL := 'https://cdn10.servertech.com/assets/documents/documents/815/original/Sentry4.mib' 57 | SYNOLOGY_URL := 'https://global.download.synology.com/download/Document/Software/DeveloperGuide/Firmware/DSM/All/enu/Synology_MIB_File.zip' 58 | UBNT_AIROS_URL := https://dl.ubnt.com/firmwares/airos-ubnt-mib/ubnt-mib.zip 59 | UBNT_AIROS_OLD_URL:= https://raw.githubusercontent.com/pgmillon/observium/refs/heads/master/mibs/FROGFOOT-RESOURCES-MIB 60 | UBNT_AIRFIBER_URL := https://dl.ubnt.com/firmwares/airfiber5X/v4.1.0/UBNT-MIB.txt 61 | UBNT_DL_URL := http://dl.ubnt-ut.com/snmp 62 | RARITAN_URL := https://cdn.raritan.com/download/PX/v1.5.20/PDU-MIB.txt 63 | RARITAN2_URL := https://cdn1.raritan.com/download/src-g2/4.0.20/PDU2_MIB_4.0.20_49038.txt 64 | INFRAPOWER_URL := https://www.austin-hughes.com/wp-content/uploads/2021/05/IPD-03-S-MIB.zip 65 | LIEBERT_URL := https://www.vertiv.com/globalassets/documents/software/monitoring/lgpmib-win_rev16_299461_0.zip 66 | READYNAS_URL := https://www.downloads.netgear.com/files/ReadyNAS/READYNAS-MIB.txt 67 | READYDATAOS_URL := https://www.downloads.netgear.com/files/GDC/RD5200/READYDATA_MIB.zip 68 | SOPHOS_XG_URL := https://docs.sophos.com/nsg/sophos-firewall/MIB/SOPHOS-XG-MIB.zip 69 | POWERCOM_URL := https://www.upspowercom.com/pcm-img/Card-DA807/Upsmate_PCM.mib 70 | TPLINK_DDM := https://static.tp-link.com/upload/software/2024/202402/20240229/L2-tplinkMibs.zip 71 | CISCO_CUCS_URL := https://raw.githubusercontent.com/cisco/cisco-mibs/refs/heads/main/ucs-mibs 72 | CISCO_CUCS_URL_v2 := https://raw.githubusercontent.com/cisco/cisco-mibs/refs/heads/main/v2 73 | YAMAHA_URL := https://www.rtpro.yamaha.co.jp/RT/docs/mib/ 74 | 75 | CYBERPOWER_VERSION := 2.11 76 | CYBERPOWER_URL := https://dl4jz3rbrsfum.cloudfront.net/software/CyberPower_MIB_v$(CYBERPOWER_VERSION).MIB.zip 77 | 78 | EAP_VERSION := 1.0 79 | EAP_URL := http://static.tp-link.com/EAP_Private_Mibs_$(EAP_VERSION).zip 80 | 81 | # https://apps.juniper.net/mib-explorer/download 82 | JUNIPER_VERSION := 24.4R1.10 83 | JUNIPER_URL := https://www.juniper.net/documentation/software/junos/junos244/juniper-mibs-$(JUNIPER_VERSION).zip 84 | 85 | .DEFAULT: all 86 | 87 | .PHONY: all 88 | all: mibs 89 | 90 | clean: 91 | rm -rvf \ 92 | $(MIBDIR)/* \ 93 | $(MIBDIR)/.cisco-device \ 94 | $(MIBDIR)/.cisco_v2 \ 95 | $(MIBDIR)/.cisco_imc \ 96 | $(MIBDIR)/.dell \ 97 | $(MIBDIR)/.dlink-mibs \ 98 | $(MIBDIR)/.dell-network \ 99 | $(MIBDIR)/.hpe-mib \ 100 | $(MIBDIR)/.net-snmp \ 101 | $(MIBDIR)/.paloalto_panos \ 102 | $(MIBDIR)/.synology \ 103 | $(MIBDIR)/.sophos_xg \ 104 | $(MIBDIR)/.kemp-lm \ 105 | $(MIBDIR)/.tplinkddm \ 106 | $(MIBDIR)/readynas \ 107 | $(MIBDIR)/readydataos \ 108 | $(MIBDIR)/.eltex-mes \ 109 | $(MIBDIR)/.juniper \ 110 | $(MIBDIR)/.yamaha-rt 111 | 112 | generator: *.go 113 | go build 114 | 115 | generate: generator mibs 116 | MIBDIRS=$(MIB_PATH) ./generator --fail-on-parse-errors generate 117 | 118 | parse_errors: generator mibs 119 | MIBDIRS=$(MIB_PATH) ./generator --fail-on-parse-errors parse_errors 120 | 121 | .PHONY: docker 122 | docker: 123 | docker build --build-arg REPO_TAG="$(REPO_TAG)" -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" . 124 | 125 | .PHONY: docker-generate 126 | docker-generate: docker mibs 127 | docker run --rm -v "${PWD}:/opt$(DOCKER_VOL_OPTS)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" --no-fail-on-parse-errors generate 128 | 129 | .PHONY: docker-publish 130 | docker-publish: 131 | docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" 132 | 133 | .PHONY: docker-tag-latest 134 | docker-tag-latest: 135 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):latest" 136 | 137 | mibs: \ 138 | $(MIBDIR)/apc-powernet-mib \ 139 | $(MIBDIR)/readynas \ 140 | $(MIBDIR)/readydataos \ 141 | $(MIBDIR)/AIRESPACE-REF-MIB \ 142 | $(MIBDIR)/AIRESPACE-WIRELESS-MIB \ 143 | $(MIBDIR)/ARISTA-ENTITY-SENSOR-MIB \ 144 | $(MIBDIR)/ARISTA-SMI-MIB \ 145 | $(MIBDIR)/ARISTA-SW-IP-FORWARDING-MIB \ 146 | $(MIBDIR)/iDRAC-SMIv2.mib \ 147 | $(MIBDIR)/.hpe-mib \ 148 | $(MIBDIR)/IANA-CHARSET-MIB.txt \ 149 | $(MIBDIR)/IANA-IFTYPE-MIB.txt \ 150 | $(MIBDIR)/IANA-PRINTER-MIB.txt \ 151 | $(MIBDIR)/FServer-Std.MIB \ 152 | $(MIBDIR)/KEEPALIVED-MIB \ 153 | $(MIBDIR)/VRRP-MIB \ 154 | $(MIBDIR)/VRRPv3-MIB \ 155 | $(MIBDIR)/.kemp-lm \ 156 | $(MIBDIR)/MIKROTIK-MIB \ 157 | $(MIBDIR)/.net-snmp \ 158 | $(MIBDIR)/.paloalto_panos \ 159 | $(MIBDIR)/PICO-IPSEC-FLOW-MONITOR-MIB.txt \ 160 | $(MIBDIR)/PICO-SMI-ID-MIB.txt \ 161 | $(MIBDIR)/PICO-SMI-MIB.txt \ 162 | $(MIBDIR)/PRINTER-MIB-V2.txt \ 163 | $(MIBDIR)/servertech-sentry3-mib \ 164 | $(MIBDIR)/servertech-sentry4-mib \ 165 | $(MIBDIR)/.synology \ 166 | $(MIBDIR)/.sophos_xg \ 167 | $(MIBDIR)/.tplinkddm \ 168 | $(MIBDIR)/UBNT-UniFi-MIB \ 169 | $(MIBDIR)/UBNT-AirFiber-MIB \ 170 | $(MIBDIR)/UBNT-AirMAX-MIB.txt \ 171 | $(MIBDIR)/PDU-MIB.txt \ 172 | $(MIBDIR)/PDU2-MIB.txt \ 173 | $(MIBDIR)/Infrapower-MIB.mib \ 174 | $(MIBDIR)/LIEBERT_GP_PDU.MIB \ 175 | $(MIBDIR)/CyberPower.MIB \ 176 | $(MIBDIR)/EAP.MIB \ 177 | $(MIBDIR)/EAP-Client.MIB \ 178 | $(MIBDIR)/powercom \ 179 | $(MIBDIR)/.cisco_imc \ 180 | $(MIBDIR)/.cisco-device \ 181 | $(MIBDIR)/FROGFOOT-RESOURCES-MIB \ 182 | $(MIBDIR)/.dlink-mibs \ 183 | $(MIBDIR)/.eltex-mes \ 184 | $(MIBDIR)/.juniper \ 185 | $(MIBDIR)/.dell-network \ 186 | $(MIBDIR)/.yamaha-rt 187 | 188 | $(MIBDIR)/apc-powernet-mib: 189 | @echo ">> Downloading apc-powernet-mib" 190 | @echo ">> if download fails please check https://www.se.com/at/de/search/?q=powernet+mib&submit+search+query=Search for the latest release" 191 | @curl $(CURL_OPTS) -o $(MIBDIR)/apc-powernet-mib "$(APC_URL)" 192 | # Workaround to make DisplayString available (#867) 193 | @sed -i.bak -E 's/(DisplayString[[:space:]]*FROM )RFC1213-MIB/\1SNMPv2-TC/' $(MIBDIR)/apc-powernet-mib 194 | @rm $(MIBDIR)/apc-powernet-mib.bak 195 | 196 | $(MIBDIR)/ARISTA-ENTITY-SENSOR-MIB: 197 | @echo ">> Downloading ARISTA-ENTITY-SENSOR-MIB" 198 | @curl $(CURL_OPTS) -o $(MIBDIR)/ARISTA-ENTITY-SENSOR-MIB "$(ARISTA_URL)/ARISTA-ENTITY-SENSOR-MIB.txt" 199 | 200 | $(MIBDIR)/ARISTA-SMI-MIB: 201 | @echo ">> Downloading ARISTA-SMI-MIB" 202 | @curl $(CURL_OPTS) -o $(MIBDIR)/ARISTA-SMI-MIB "$(ARISTA_URL)/ARISTA-SMI-MIB.txt" 203 | 204 | $(MIBDIR)/ARISTA-SW-IP-FORWARDING-MIB: 205 | @echo ">> Downloading ARISTA-SW-IP-FORWARDING-MIB" 206 | @curl $(CURL_OPTS) -o $(MIBDIR)/ARISTA-SW-IP-FORWARDING-MIB "$(ARISTA_URL)/ARISTA-SW-IP-FORWARDING-MIB.txt" 207 | 208 | $(MIBDIR)/AIRESPACE-REF-MIB: 209 | @echo ">> Downloading Cisco AIRESPACE-REF-MIB" 210 | @curl $(CURL_OPTS) -o $(MIBDIR)/AIRESPACE-REF-MIB "$(CISCO_URL)/AIRESPACE-REF-MIB.my" 211 | 212 | $(MIBDIR)/AIRESPACE-WIRELESS-MIB: 213 | @echo ">> Downloading Cisco AIRESPACE-WIRELESS-MIB" 214 | @curl $(CURL_OPTS) -o $(MIBDIR)/AIRESPACE-WIRELESS-MIB "$(CISCO_URL)/AIRESPACE-WIRELESS-MIB.my" 215 | 216 | $(MIBDIR)/iDRAC-SMIv2.mib: 217 | $(eval TMP := $(shell mktemp)) 218 | @echo ">> Downloading dell to $(TMP)" 219 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(TMP) $(DELL_URL) 220 | @unzip -j -d $(MIBDIR) $(TMP) support/station/mibs/iDRAC-SMIv2.mib 221 | @rm -v $(TMP) 222 | 223 | $(MIBDIR)/.hpe-mib: 224 | $(eval TMP := $(shell mktemp)) 225 | $(eval TMP_DIR := $(shell mktemp -d)) 226 | @echo ">> Downloading HPE to $(TMP)" 227 | @curl -L $(CURL_OPTS) $(CURL_USER_AGENT) -o $(TMP) $(HPE_URL) 228 | @tar -xf $(TMP) -C $(TMP_DIR) 229 | @mv $(TMP_DIR)/*cpq*.mib $(MIBDIR) 230 | @rm -rf $(TMP_DIR) $(TMP) 231 | @touch $(MIBDIR)/.hpe-mib 232 | 233 | $(MIBDIR)/FServer-Std.MIB: 234 | $(eval TMP := $(shell mktemp)) 235 | @echo ">> Downloading FIELDSERVER MIB to $(TMP)" 236 | @curl $(CURL_OPTS) -o $(TMP) $(FIELDSERVER_URL) 237 | @unzip -j -d $(MIBDIR) $(TMP) FServer-Std.MIB 238 | # Remove invalid line in the MIB 239 | @sed -i.bak '/----/d' $(MIBDIR)/FServer-Std.MIB 240 | # Fix MIB - it claims that these tables have two indices but the data returned 241 | # from the device only has a single index. 242 | @sed -i -r \ 243 | '/(analog|binary)(Inputs|Outputs|Values)Entry OBJECT-TYPE/,/::=/ { /[ab][iov]Description/d }' \ 244 | $(MIBDIR)/FServer-Std.MIB 245 | @rm -v $(TMP) 246 | 247 | $(MIBDIR)/IANA-CHARSET-MIB.txt: 248 | @echo ">> Downloading IANA charset MIB" 249 | @curl $(CURL_OPTS) -o $(MIBDIR)/IANA-CHARSET-MIB.txt $(IANA_CHARSET_URL) 250 | 251 | $(MIBDIR)/IANA-IFTYPE-MIB.txt: 252 | @echo ">> Downloading IANA ifType MIB" 253 | @curl $(CURL_OPTS) -o $(MIBDIR)/IANA-IFTYPE-MIB.txt $(IANA_IFTYPE_URL) 254 | 255 | $(MIBDIR)/IANA-PRINTER-MIB.txt: 256 | @echo ">> Downloading IANA printer MIB" 257 | @curl $(CURL_OPTS) -o $(MIBDIR)/IANA-PRINTER-MIB.txt $(IANA_PRINTER_URL) 258 | 259 | $(MIBDIR)/KEEPALIVED-MIB: 260 | @echo ">> Downloading KEEPALIVED-MIB" 261 | @curl $(CURL_OPTS) -o $(MIBDIR)/KEEPALIVED-MIB $(KEEPALIVED_URL) 262 | 263 | $(MIBDIR)/VRRP-MIB: 264 | @echo ">> Downloading VRRP-MIB" 265 | @curl $(CURL_OPTS) -o $(MIBDIR)/VRRP-MIB $(VRRP_URL) 266 | 267 | $(MIBDIR)/VRRPv3-MIB: 268 | @echo ">> Downloading VRRPv3-MIB" 269 | @curl $(CURL_OPTS) -o $(MIBDIR)/VRRPv3-MIB $(VRRPV3_URL) 270 | 271 | $(MIBDIR)/.kemp-lm: 272 | $(eval TMP := $(shell mktemp)) 273 | @echo ">> Downloading Kemp LM MIBs to $(TMP)" 274 | @curl $(CURL_OPTS) -L -o $(TMP) $(KEMP_LM_URL) 275 | @unzip -j -d $(MIBDIR) $(TMP) *.txt 276 | # Workaround invalid timestamps. 277 | @sed -i.bak -E 's/"([0-9]{12})[0-9]{2}Z"/"\1Z"/' $(MIBDIR)/*.RELEASE-B100-MIB.txt 278 | @rm $(MIBDIR)/*.RELEASE-B100-MIB.txt.bak 279 | @rm -v $(TMP) 280 | @touch $(MIBDIR)/.kemp-lm 281 | 282 | $(MIBDIR)/MIKROTIK-MIB: 283 | @echo ">> Downloading MIKROTIK-MIB" 284 | @curl $(CURL_OPTS) -L -o $(MIBDIR)/MIKROTIK-MIB $(MIKROTIK_URL) 285 | 286 | $(MIBDIR)/.net-snmp: 287 | @echo ">> Downloading NET-SNMP mibs" 288 | @curl $(CURL_OPTS) -o $(MIBDIR)/HCNUM-TC $(NET_SNMP_URL)/HCNUM-TC.txt 289 | @curl $(CURL_OPTS) -o $(MIBDIR)/HOST-RESOURCES-MIB $(NET_SNMP_URL)/HOST-RESOURCES-MIB.txt 290 | @curl $(CURL_OPTS) -o $(MIBDIR)/IF-MIB $(NET_SNMP_URL)/IF-MIB.txt 291 | @curl $(CURL_OPTS) -o $(MIBDIR)/IP-MIB $(NET_SNMP_URL)/IP-MIB.txt 292 | @curl $(CURL_OPTS) -o $(MIBDIR)/INET-ADDRESS-MIB $(NET_SNMP_URL)/INET-ADDRESS-MIB.txt 293 | @curl $(CURL_OPTS) -o $(MIBDIR)/IPV6-TC $(NET_SNMP_URL)/IPV6-TC.txt 294 | @curl $(CURL_OPTS) -o $(MIBDIR)/NET-SNMP-MIB $(NET_SNMP_URL)/NET-SNMP-MIB.txt 295 | @curl $(CURL_OPTS) -o $(MIBDIR)/NET-SNMP-TC $(NET_SNMP_URL)/NET-SNMP-TC.txt 296 | @curl $(CURL_OPTS) -o $(MIBDIR)/SNMP-FRAMEWORK-MIB $(NET_SNMP_URL)/SNMP-FRAMEWORK-MIB.txt 297 | @curl $(CURL_OPTS) -o $(MIBDIR)/SNMPv2-MIB $(NET_SNMP_URL)/SNMPv2-MIB.txt 298 | @curl $(CURL_OPTS) -o $(MIBDIR)/SNMPv2-SMI $(NET_SNMP_URL)/SNMPv2-SMI.txt 299 | @curl $(CURL_OPTS) -o $(MIBDIR)/SNMPv2-TC $(NET_SNMP_URL)/SNMPv2-TC.txt 300 | @curl $(CURL_OPTS) -o $(MIBDIR)/UCD-SNMP-MIB $(NET_SNMP_URL)/UCD-SNMP-MIB.txt 301 | @touch $(MIBDIR)/.net-snmp 302 | 303 | $(MIBDIR)/PICO-IPSEC-FLOW-MONITOR-MIB.txt: 304 | @echo ">> Downloading PICO-IPSEC-FLOW-MONITOR-MIB.txt" 305 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/PICO-IPSEC-FLOW-MONITOR-MIB.txt "$(NEC_URL)/PICO-IPSEC-FLOW-MONITOR-MIB.txt" 306 | 307 | $(MIBDIR)/PICO-SMI-MIB.txt: 308 | @echo ">> Downloading PICO-SMI-MIB.txt" 309 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/PICO-SMI-MIB.txt "$(NEC_URL)/PICO-SMI-MIB.txt" 310 | 311 | $(MIBDIR)/PICO-SMI-ID-MIB.txt: 312 | @echo ">> Downloading PICO-SMI-ID-MIB.txt" 313 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/PICO-SMI-ID-MIB.txt "$(NEC_URL)/PICO-SMI-ID-MIB.txt" 314 | 315 | $(MIBDIR)/.paloalto_panos: 316 | $(eval TMP := $(shell mktemp)) 317 | @echo ">> Downloading paloalto_pano to $(TMP)" 318 | @curl $(CURL_OPTS) -o $(TMP) $(PALOALTO_URL) 319 | @unzip -j -d $(MIBDIR) $(TMP) 320 | @rm -v $(TMP) 321 | @touch $(MIBDIR)/.paloalto_panos 322 | 323 | $(MIBDIR)/PRINTER-MIB-V2.txt: 324 | @echo ">> Downloading Printer MIB v2" 325 | @curl $(CURL_OPTS) -o $(MIBDIR)/PRINTER-MIB-V2.txt $(PRINTER_URL) 326 | 327 | $(MIBDIR)/servertech-sentry3-mib: 328 | @echo ">> Downloading servertech-sentry3-mib" 329 | @curl $(CURL_OPTS) -o $(MIBDIR)/servertech-sentry3-mib $(SERVERTECH_URL) 330 | 331 | $(MIBDIR)/servertech-sentry4-mib: 332 | @echo ">> Downloading servertech-sentry4-mib" 333 | @curl $(CURL_OPTS) -o $(MIBDIR)/servertech-sentry4-mib $(SERVERTECH4_URL) 334 | 335 | $(MIBDIR)/.sophos_xg: 336 | $(eval TMP := $(shell mktemp)) 337 | @echo ">> Downloading Sophos XG to $(TMP)" 338 | @curl $(CURL_OPTS) -o $(TMP) $(SOPHOS_XG_URL) 339 | @unzip -j -d $(MIBDIR) $(TMP) sophos-xg-mib/SOPHOS-XG-MIB20.mib 340 | @rm -v $(TMP) 341 | @touch $(MIBDIR)/.sophos_xg 342 | 343 | $(MIBDIR)/.synology: 344 | $(eval TMP := $(shell mktemp)) 345 | @echo ">> Downloading synology to $(TMP)" 346 | @curl $(CURL_OPTS) -o $(TMP) $(SYNOLOGY_URL) 347 | @unzip -j -d $(MIBDIR) $(TMP) 348 | @rm -v $(TMP) 349 | @touch $(MIBDIR)/.synology 350 | 351 | $(MIBDIR)/.tplinkddm: 352 | $(eval TMP := $(shell mktemp)) 353 | @echo ">> Downloading TPLINK Switch DDM to $(TMP)" 354 | @curl $(CURL_OPTS) -o $(TMP) $(TPLINK_DDM) 355 | @unzip -j -d $(MIBDIR) $(TMP) L2-tplinkMibs/tplink-ddmManage.mib L2-tplinkMibs/tplink-ddmStatus.mib L2-tplinkMibs/tplink.mib 356 | @mv $(MIBDIR)/tplink.mib $(MIBDIR)/tplink-main.mib # EAP.MIB contains another incompatible variant of the tplink enterprise mib with same name 357 | @rm -v $(TMP) 358 | @touch $(MIBDIR)/.tplinkddm 359 | 360 | $(MIBDIR)/UBNT-UniFi-MIB: 361 | @echo ">> Downloading UBNT-UniFi-MIB" 362 | @curl $(CURL_OPTS) -o $(MIBDIR)/UBNT-UniFi-MIB "$(UBNT_DL_URL)/UBNT-UniFi-MIB" 363 | 364 | $(MIBDIR)/UBNT-AirFiber-MIB: 365 | @echo ">> Downloading UBNT-AirFiber-MIB" 366 | @curl $(CURL_OPTS) -o $(MIBDIR)/UBNT-AirFiber-MIB $(UBNT_AIRFIBER_URL) 367 | 368 | $(MIBDIR)/FROGFOOT-RESOURCES-MIB: 369 | @echo ">> Downloading FROGFOOT-RESOURCES-MIB (UBNT AirOS)" 370 | @curl $(CURL_OPTS) -o $(MIBDIR)/FROGFOOT-RESOURCES-MIB $(UBNT_AIROS_OLD_URL) 371 | 372 | $(MIBDIR)/.dlink-mibs: 373 | @echo ">> Downloading DLINK mibs" 374 | $(eval TMP := $(shell mktemp)) 375 | @echo ">> Downloading DLINK DES mibs to $(TMP)" 376 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(TMP) $(DLINK_URL) 377 | @unzip -j -d $(MIBDIR) $(TMP) \ 378 | DES3200_V4.04_MIB_20120906/standardMIB/RMON2-MIB.MIB \ 379 | DES3200_V4.04_MIB_20120906/standardMIB/RMON-MIB.mib \ 380 | DES3200_V4.04_MIB_20120906/standardMIB/token-ring-rmon-mib.MIB \ 381 | DES3200_V4.04_MIB_20120906/standardMIB/P-BRIDGE-MIB.mib \ 382 | DES3200_V4.04_MIB_20120906/standardMIB/BRIDGE-MIB.mib \ 383 | DES3200_V4.04_MIB_20120906/standardMIB/Q-BRIDGE-MIB.mib \ 384 | DES3200_V4.04_MIB_20120906/proprietaryMIB/Dlink-common-mgmt/Genmgmt.mib \ 385 | DES3200_V4.04_MIB_20120906/proprietaryMIB/Dlink-mgmt/DLINK-ID-REC-MIB.mib 386 | @rm -v $(TMP) 387 | @touch $(MIBDIR)/.dlink-mibs 388 | 389 | 390 | $(MIBDIR)/UBNT-AirMAX-MIB.txt: 391 | $(eval TMP := $(shell mktemp)) 392 | @echo ">> Downloading ubnt-airos to $(TMP)" 393 | @curl $(CURL_OPTS) -o $(TMP) $(UBNT_AIROS_URL) 394 | @unzip -j -d $(MIBDIR) $(TMP) UBNT-AirMAX-MIB.txt 395 | @rm -v $(TMP) 396 | 397 | $(MIBDIR)/PDU-MIB.txt: 398 | @echo ">> Downloading Raritan PDU-MIB" 399 | @curl $(CURL_OPTS) -o $(MIBDIR)/PDU-MIB.txt "$(RARITAN_URL)" 400 | 401 | $(MIBDIR)/PDU2-MIB.txt: 402 | @echo ">> Downloading Raritan PDU2-MIB" 403 | @curl $(CURL_OPTS) -o $(MIBDIR)/PDU2-MIB.txt "$(RARITAN2_URL)" 404 | 405 | $(MIBDIR)/Infrapower-MIB.mib: 406 | $(eval TMP := $(shell mktemp)) 407 | @echo ">> Downloading Infrapower-MIB.mib to $(TMP)" 408 | @rm -vf $(MIBDIR)/IPD-MIB_Q419V9.mib 409 | @curl $(CURL_OPTS) -L -o $(TMP) $(INFRAPOWER_URL) 410 | @unzip -j -d $(MIBDIR) $(TMP) IPD-03-S-MIB_Q320V1.mib 411 | @mv -v $(MIBDIR)/IPD-03-S-MIB_Q320V1.mib $(MIBDIR)/Infrapower-MIB.mib 412 | @rm -v $(TMP) 413 | 414 | $(MIBDIR)/LIEBERT_GP_PDU.MIB: 415 | $(eval TMP := $(shell mktemp)) 416 | @echo ">> Downloading LIEBERT_GP_PDU.MIB to $(TMP)" 417 | @curl $(CURL_OPTS) -o $(TMP) $(LIEBERT_URL) 418 | @unzip -j -d $(MIBDIR) $(TMP) LIEBERT_GP_PDU.MIB LIEBERT_GP_REG.MIB 419 | @rm -v $(TMP) 420 | 421 | $(MIBDIR)/CyberPower.MIB: 422 | $(eval TMP := $(shell mktemp)) 423 | @echo ">> Downloading CyberPower.MIB to $(TMP)" 424 | @curl $(CURL_OPTS) -o $(TMP) $(CYBERPOWER_URL) 425 | @unzip -j -d $(MIBDIR) $(TMP) CyberPower_MIB_v$(CYBERPOWER_VERSION).MIB 426 | @mv -v $(MIBDIR)/CyberPower_MIB_v$(CYBERPOWER_VERSION).MIB $@ 427 | # Workaround to make DisplayString available (#867) 428 | @sed -i.bak -E 's/(DisplayString[[:space:]]*FROM )RFC1213-MIB/\1SNMPv2-TC/' $@ 429 | @rm $@.bak 430 | @rm -v $(TMP) 431 | 432 | $(MIBDIR)/EAP.MIB $(MIBDIR)/EAP-Client.MIB: 433 | $(eval TMP := $(shell mktemp)) 434 | @echo ">> Downloading EAP.MIB to $(TMP)" 435 | @curl $(CURL_OPTS) -o $(TMP) $(EAP_URL) 436 | @unzip -j -d $(MIBDIR) $(TMP) EAP_Private-Mibs_$(EAP_VERSION)/TPLINK.mib EAP_Private-Mibs_$(EAP_VERSION)/EAP/clientMib/client.mib 437 | @mv -v $(MIBDIR)/TPLINK.mib $(MIBDIR)/EAP.MIB 438 | @mv -v $(MIBDIR)/client.mib $(MIBDIR)/EAP-Client.MIB 439 | @rm -v $(TMP) 440 | 441 | $(MIBDIR)/readynas: 442 | @echo ">> Downloading readynas" 443 | @curl $(CURL_OPTS) -o $(MIBDIR)/readynas "$(READYNAS_URL)" 444 | 445 | $(MIBDIR)/readydataos: 446 | $(eval TMP := $(shell mktemp)) 447 | @echo ">> Downloading readydataos to $(TMP)" 448 | @curl $(CURL_OPTS) -o $(TMP) "$(READYDATAOS_URL)" 449 | @unzip -j -d $(MIBDIR) $(TMP) READYDATAOS-MIB.txt 450 | @mv -v $(MIBDIR)/READYDATAOS-MIB.txt $(MIBDIR)/readydataos 451 | @rm -v $(TMP) 452 | 453 | $(MIBDIR)/powercom: 454 | @echo ">> Downloading powercom" 455 | @curl $(CURL_OPTS) -o $(MIBDIR)/powercom "$(POWERCOM_URL)" 456 | 457 | $(MIBDIR)/.cisco_imc: 458 | @echo ">> Downloading Cisco UCS" 459 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-UNIFIED-COMPUTING-FAULT-MIB.my "$(CISCO_CUCS_URL)/CISCO-UNIFIED-COMPUTING-FAULT-MIB.my" 460 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-UNIFIED-COMPUTING-MIB.my "$(CISCO_CUCS_URL)/CISCO-UNIFIED-COMPUTING-MIB.my" 461 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-UNIFIED-COMPUTING-STORAGE-MIB.my "$(CISCO_CUCS_URL)/CISCO-UNIFIED-COMPUTING-STORAGE-MIB.my" 462 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-UNIFIED-COMPUTING-TC-MIB.my "$(CISCO_CUCS_URL)/CISCO-UNIFIED-COMPUTING-TC-MIB.my" 463 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-TC.my "$(CISCO_CUCS_URL_v2)/CISCO-TC.my" 464 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-SMI.my "$(CISCO_CUCS_URL_v2)/CISCO-SMI.my" 465 | @touch $(MIBDIR)/.cisco_imc 466 | 467 | $(MIBDIR)/.eltex-mes: 468 | $(eval TMP := $(shell mktemp)) 469 | @echo ">> Downloading Eltex MES device mibs to $(TMP)" 470 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(TMP) $(ELTEX_MES_URL) 471 | @unzip -j -d $(MIBDIR) $(TMP) fsiss.mib eltex/ELTEX-SMI-ACTUAL.mib eltex/ELTEX-MES-ISS-CPU-UTIL-MIB.mib eltex/ELTEX-MES-ISS-MIB.mib CISCO-QOS-PIB-MIB.mib 472 | @rm -v $(TMP) 473 | @touch $(MIBDIR)/.eltex-mes 474 | 475 | $(MIBDIR)/.juniper: 476 | $(eval TMP := $(shell mktemp)) 477 | @echo ">> Downloading Juniper mibs to $(TMP)" 478 | @curl $(CURL_OPTS) -o $(TMP) $(JUNIPER_URL) 479 | @unzip -j -d $(MIBDIR) $(TMP) \ 480 | StandardMibs/mib-alarmmib.txt \ 481 | StandardMibs/mib-rfc2819a.txt \ 482 | StandardMibs/mib-rfc4502.txt \ 483 | StandardMibs/mib-rfc1513.txt \ 484 | JuniperMibs/mib-jnx-smi.txt \ 485 | JuniperMibs/mib-jnx-chassis.txt \ 486 | JuniperMibs/mib-jnx-alarm.txt \ 487 | JuniperMibs/mib-jnx-chassis-alarm.txt \ 488 | JuniperMibs/mib-jnx-dom.txt \ 489 | JuniperMibs/mib-jnx-subscriber.txt 490 | @rm -v $(TMP) 491 | @touch $(MIBDIR)/.juniper 492 | 493 | $(MIBDIR)/.dell-network: 494 | $(eval TMP := $(shell mktemp)) 495 | @echo ">> Downloading Dell network mibs to $(TMP)" 496 | @curl $(CURL_OPTS) -o $(TMP) $(DELL_NETWORK_URL) 497 | @unzip -j -d $(MIBDIR)/dell $(TMP) DELL-NETWORKING-MIB-9.14.2.1.zip 498 | @unzip -j -d $(MIBDIR) $(MIBDIR)/dell/DELL-NETWORKING-MIB-9.14.2.1.zip \ 499 | DELL-NETWORKING-CHASSIS-MIB.mib \ 500 | DELL-NETWORKING-TC.mib \ 501 | DELL-NETWORKING-SMI.mib 502 | @rm -rfv $(TMP) $(MIBDIR)/dell 503 | @touch $(MIBDIR)/.dell-network 504 | 505 | # sed below fixes CISCO-FC-FE-MIB mib (ref: https://github.com/cisco/cisco-mibs/issues/136) 506 | $(MIBDIR)/.cisco-device: 507 | @echo ">> Downloading Cisco device mibs" 508 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-TC $(CISCO_URL)/CISCO-TC.my 509 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-SMI $(CISCO_URL)/CISCO-SMI.my 510 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-PROCESS-MIB $(CISCO_URL)/CISCO-PROCESS-MIB.my 511 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-ENVMON-MIB $(CISCO_URL)/CISCO-ENVMON-MIB.my 512 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-ENTITY-SENSOR-MIB $(CISCO_URL)/CISCO-ENTITY-SENSOR-MIB.my 513 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-MEMORY-POOL-MIB $(CISCO_URL)/CISCO-MEMORY-POOL-MIB.my 514 | @curl $(CURL_OPTS) -o $(MIBDIR)/ENTITY-MIB $(CISCO_URL)/ENTITY-MIB.my 515 | @curl $(CURL_OPTS) -o $(MIBDIR)/ENTITY-SENSOR-MIB $(CISCO_URL)/ENTITY-SENSOR-MIB.my 516 | @curl $(CURL_OPTS) -o $(MIBDIR)/ENTITY-STATE-MIB $(CISCO_URL)/ENTITY-STATE-MIB.my 517 | @curl $(CURL_OPTS) -o $(MIBDIR)/ENTITY-STATE-TC-MIB $(CISCO_URL)/ENTITY-STATE-TC-MIB.my 518 | @curl $(CURL_OPTS) -o $(MIBDIR)/ISDN-MIB "$(CISCO_URL)/ISDN-MIB.my" 519 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-MEMORY-POOL-MIB $(CISCO_URL)/CISCO-MEMORY-POOL-MIB.my 520 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-ENHANCED-MEMPOOL-MIB $(CISCO_URL)/CISCO-ENHANCED-MEMPOOL-MIB.my 521 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-ENTITY-FRU-CONTROL-MIB $(CISCO_URL)/CISCO-ENTITY-FRU-CONTROL-MIB.my 522 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-FC-FE-MIB $(CISCO_URL)/CISCO-FC-FE-MIB.my 523 | @sed -i -E 's/OBJECT.+TransceiverPowerControl/OBJECT fcIfTransceiverPowerControl/' $(MIBDIR)/CISCO-FC-FE-MIB 524 | @sed -i -E -z 's/(fcIfSysTransceiverPowerControlCapability,\n.+fcIfSysTransceiverPowerControl),/\1/' $(MIBDIR)/CISCO-FC-FE-MIB 525 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-ST-TC $(CISCO_URL)/CISCO-ST-TC.my 526 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-VSAN-MIB $(CISCO_URL)/CISCO-VSAN-MIB.my 527 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-ZS-MIB $(CISCO_URL)/CISCO-ZS-MIB.my 528 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-IF-EXTENSION-MIB $(CISCO_URL)/CISCO-IF-EXTENSION-MIB.my 529 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-NS-MIB $(CISCO_URL)/CISCO-NS-MIB.my 530 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-QOS-PIB-MIB $(CISCO_URL)/CISCO-QOS-PIB-MIB.my 531 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-IMAGE-TC $(CISCO_URL)/CISCO-IMAGE-TC.my 532 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-IMAGE-MIB $(CISCO_URL)/CISCO-IMAGE-MIB.my 533 | @curl $(CURL_OPTS) -o $(MIBDIR)/CISCO-ENHANCED-IMAGE-MIB $(CISCO_URL)/CISCO-ENHANCED-IMAGE-MIB.my 534 | @touch $(MIBDIR)/.cisco-device 535 | 536 | $(MIBDIR)/.yamaha-rt: 537 | @echo ">> Downloading Yamaha RT Series MIBs" 538 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/yamaha-product.mib.txt $(YAMAHA_URL)/yamaha-product.mib.txt 539 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/yamaha-rt.mib.txt $(YAMAHA_URL)/yamaha-rt.mib.txt 540 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/yamaha-rt-firmware.mib.txt $(YAMAHA_URL)/yamaha-rt-firmware.mib.txt 541 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/yamaha-rt-hardware.mib.txt $(YAMAHA_URL)/yamaha-rt-hardware.mib.txt 542 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/yamaha-rt-interfaces.mib.txt $(YAMAHA_URL)/yamaha-rt-interfaces.mib.txt 543 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/yamaha-rt-ip.mib.txt $(YAMAHA_URL)/yamaha-rt-ip.mib.txt 544 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/yamaha-rt-switch.mib.txt $(YAMAHA_URL)/yamaha-rt-switch.mib.txt 545 | @curl $(CURL_OPTS) $(CURL_USER_AGENT) -o $(MIBDIR)/yamaha-smi.mib.txt $(YAMAHA_URL)/yamaha-smi.mib.txt 546 | # Workaround to make DisplayString available (#867) 547 | @find $(MIBDIR) -name 'yamaha-*.mib.txt' | xargs sed -i.bak -z -E 's/(DisplayString(, PhysAddress)?[[:space:]\n]*FROM )RFC1213-MIB/\1SNMPv2-TC/' 548 | @rm $(MIBDIR)/yamaha-*.mib.txt.bak 549 | @touch $(MIBDIR)/.yamaha-rt 550 | -------------------------------------------------------------------------------- /generator/README.md: -------------------------------------------------------------------------------- 1 | 2 | # SNMP Exporter Config Generator 3 | 4 | This config generator uses NetSNMP to parse MIBs, and generates configs for the snmp_exporter using them. 5 | 6 | ## Building 7 | 8 | Due to the dynamic dependency on NetSNMP, you must build the generator yourself. 9 | 10 | ``` 11 | # Debian-based distributions. 12 | sudo apt-get install unzip build-essential libsnmp-dev # Debian-based distros 13 | # Redhat-based distributions. 14 | sudo yum install gcc make net-snmp net-snmp-utils net-snmp-libs net-snmp-devel # RHEL-based distros 15 | 16 | git clone https://github.com/prometheus/snmp_exporter.git 17 | cd snmp_exporter/generator 18 | make generator mibs 19 | ``` 20 | ## Preparation 21 | 22 | It is recommended to have a directory per device family which contains the mibs dir for the device family, 23 | a logical link to the generator executable and the generator.yml configuration file. This is to avoid name space collisions 24 | in the MIB definition. Keep only the required MIBS in the mibs directory for the devices. 25 | Then merge all the resulting snmp.yml files into one main file that will be used by the snmp_exporter collector. 26 | 27 | ## Running 28 | 29 | ```sh 30 | make generate 31 | ``` 32 | 33 | The generator reads in the simplified collection instructions from `generator.yml` and writes to `snmp.yml`. Only the snmp.yml file is used 34 | by the snmp_exporter executable to collect data from the snmp enabled devices. 35 | 36 | Additional command are available for debugging, use the `help` command to see them. 37 | 38 | After building, you can pass a directories of mibs, a path to the `generator.yml` 39 | file and the intended path of your output file e.g. `snmp.yml` to the `generate` 40 | command like so, 41 | ```bash 42 | ./generator generate \ 43 | -m /tmp/deviceFamilyMibs \ 44 | -m /tmp/sharedMibs \ 45 | -g /tmp/generator.yml \ 46 | -o /tmp/snmp.yml 47 | ``` 48 | 49 | ### MIB Parsing options 50 | 51 | The parsing of MIBs can be controlled using the `--snmp.mibopts` flag. The available values depend on the net-snmp version used to build the generator. 52 | 53 | Example from net-snmp 5.9.1: 54 | 55 | ``` 56 | Toggle various defaults controlling MIB parsing: 57 | u: allow the use of underlines in MIB symbols 58 | c: disallow the use of "--" to terminate comments 59 | d: save the DESCRIPTIONs of the MIB objects 60 | e: disable errors when MIB symbols conflict 61 | w: enable warnings when MIB symbols conflict 62 | W: enable detailed warnings when MIB symbols conflict 63 | R: replace MIB symbols from latest module 64 | ``` 65 | 66 | ## Docker Users 67 | 68 | If you would like to run the generator in docker to generate your `snmp.yml` config run the following commands. 69 | 70 | The Docker image expects a directory containing the `generator.yml` and a directory called `mibs` that contains all MIBs you wish to use. 71 | 72 | This example will generate the example `snmp.yml` which is included in the top level of the snmp_exporter repo: 73 | 74 | ```sh 75 | make docker-generate 76 | ``` 77 | 78 | ## File Format 79 | 80 | `generator.yml` provides a list of auths and modules. Each module defines what to collect from a device type. 81 | The simplest module is just a name and a set of OIDs to walk. 82 | 83 | ```yaml 84 | auths: 85 | auth_name: 86 | version: 2 # SNMP version to use. Defaults to 2. 87 | # 1 will use GETNEXT, 2 and 3 use GETBULK. 88 | 89 | # Community string is used with SNMP v1 and v2. Defaults to "public". 90 | community: public 91 | 92 | # v3 has different and more complex settings. 93 | # Which are required depends on the security_level. 94 | # The equivalent options on NetSNMP commands like snmpbulkwalk 95 | # and snmpget are also listed. See snmpcmd(1). 96 | username: user # Required, no default. -u option to NetSNMP. 97 | security_level: noAuthNoPriv # Defaults to noAuthNoPriv. -l option to NetSNMP. 98 | # Can be noAuthNoPriv, authNoPriv or authPriv. 99 | password: pass # Has no default. Also known as authKey, -A option to NetSNMP. 100 | # Required if security_level is authNoPriv or authPriv. 101 | auth_protocol: MD5 # MD5, SHA, SHA224, SHA256, SHA384, or SHA512. Defaults to MD5. -a option to NetSNMP. 102 | # Used if security_level is authNoPriv or authPriv. 103 | priv_protocol: DES # DES, AES, AES192, AES256, AES192C, or AES256C. Defaults to DES. -x option to NetSNMP. 104 | # Used if security_level is authPriv. 105 | priv_password: otherPass # Has no default. Also known as privKey, -X option to NetSNMP. 106 | # Required if security_level is authPriv. 107 | context_name: context # Has no default. -n option to NetSNMP. 108 | # Required if context is configured on the device. 109 | 110 | modules: 111 | module_name: # The module name. You can have as many modules as you want. 112 | # List of OIDs to walk. Can also be SNMP object names or specific instances. 113 | # Object names can be fully-qualified with the MIB name separated by `::`. 114 | walk: 115 | - 1.3.6.1.2.1.2 # Same as "interfaces" 116 | - "SNMPv2-MIB::sysUpTime" # Same as "1.3.6.1.2.1.1.3" 117 | - 1.3.6.1.2.1.31.1.1.1.6.40 # Instance of "ifHCInOctets" with index "40" 118 | - 1.3.6.1.2.1.2.2.1.4 # Same as ifMtu (used for filter example) 119 | - bsnDot11EssSsid # Same as 1.3.6.1.4.1.14179.2.1.1.1.2 (used for filter example) 120 | 121 | max_repetitions: 25 # How many objects to request with GET/GETBULK, defaults to 25. 122 | # May need to be reduced for buggy devices. 123 | retries: 3 # How many times to retry a failed request, defaults to 3. 124 | timeout: 5s # Timeout for each individual SNMP request, defaults to 5s. 125 | 126 | allow_nonincreasing_oids: false # Do not check whether the returned OIDs are increasing, defaults to false 127 | # Some agents return OIDs out of order, but can complete the walk anyway. 128 | # -Cc option of NetSNMP 129 | 130 | use_unconnected_udp_socket: false # Use a unconnected udp socket, defaults to false 131 | # Some multi-homed network gear isn't smart enough to send SNMP responses 132 | # from the address it received the requests on. To work around that, 133 | # we can open unconnected UDP socket and use sendto/recvfrom 134 | 135 | lookups: # Optional list of lookups to perform. 136 | # The default for `keep_source_indexes` is false. Indexes must be unique for this option to be used. 137 | 138 | # If the index of a table is bsnDot11EssIndex, usually that'd be the label 139 | # on the resulting metrics from that table. Instead, use the index to 140 | # lookup the bsnDot11EssSsid table entry and create a bsnDot11EssSsid label 141 | # with that value. 142 | - source_indexes: [bsnDot11EssIndex] 143 | lookup: bsnDot11EssSsid 144 | drop_source_indexes: false # If true, delete source index labels for this lookup. 145 | # This avoids label clutter when the new index is unique. 146 | 147 | # It is also possible to chain lookups or use multiple labels to gather label values. 148 | # This might be helpful to resolve multiple index labels to a proper human readable label. 149 | # Please be aware that ordering matters here. 150 | 151 | # In this example, we first do a lookup to get the `cbQosConfigIndex` as another label. 152 | - source_indexes: [cbQosPolicyIndex, cbQosObjectsIndex] 153 | lookup: cbQosConfigIndex 154 | # Using the newly added label, we have another lookup to fetch the `cbQosCMName` based on `cbQosConfigIndex`. 155 | - source_indexes: [cbQosConfigIndex] 156 | lookup: cbQosCMName 157 | 158 | overrides: # Allows for per-module overrides of bits of MIBs 159 | metricName: 160 | ignore: true # Drops the metric from the output. 161 | help: "string" # Override the generated HELP text provided by the MIB Description. 162 | name: "string" # Override the OID name provided in the MIB Description. 163 | regex_extracts: 164 | Temp: # A new metric will be created appending this to the metricName to become metricNameTemp. 165 | - regex: '(.*)' # Regex to extract a value from the returned SNMP walks's value. 166 | value: '$1' # The result will be parsed as a float64, defaults to $1. 167 | Status: 168 | - regex: '.*Example' 169 | value: '1' # The first entry whose regex matches and whose value parses wins. 170 | - regex: '.*' 171 | value: '0' 172 | datetime_pattern: # Used if type = ParseDateAndTime. Uses the strptime format (See: man 3 strptime) 173 | offset: 1.0 # Add the value to the same. Applied after scale. 174 | scale: 1.0 # Scale the value of the sample by this value. 175 | type: DisplayString # Override the metric type, possible types are: 176 | # gauge: An integer with type gauge. 177 | # counter: An integer with type counter. 178 | # OctetString: A bit string, rendered as 0xff34. 179 | # DateAndTime: An RFC 2579 DateAndTime byte sequence. If the device has no time zone data, UTC is used. 180 | # ParseDateAndTime: Parse a DisplayString and return the timestamp. See datetime_pattern config option 181 | # NTPTimeStamp: Parse the NTP timestamp (RFC-1305, March 1992, Section 3.1) and return Unix timestamp as float. 182 | # DisplayString: An ASCII or UTF-8 string. 183 | # PhysAddress48: A 48 bit MAC address, rendered as 00:01:02:03:04:ff. 184 | # Float: A 32 bit floating-point value with type gauge. 185 | # Double: A 64 bit floating-point value with type gauge. 186 | # InetAddressIPv4: An IPv4 address, rendered as 192.0.0.8. 187 | # InetAddressIPv6: An IPv6 address, rendered as 0102:0304:0506:0708:090A:0B0C:0D0E:0F10. 188 | # InetAddress: An InetAddress per RFC 4001. Must be preceded by an InetAddressType. 189 | # InetAddressMissingSize: An InetAddress that violates section 4.1 of RFC 4001 by 190 | # not having the size in the index. Must be preceded by an InetAddressType. 191 | # EnumAsInfo: An enum for which a single timeseries is created. Good for constant values. 192 | # EnumAsStateSet: An enum with a time series per state. Good for variable low-cardinality enums. 193 | # Bits: An RFC 2578 BITS construct, which produces a StateSet with a time series per bit. 194 | 195 | filters: # Define filters to collect only a subset of OID table indices 196 | static: # static filters are handled in the generator. They will convert walks to multiple gets with the specified indices 197 | # in the resulting snmp.yml output. 198 | # the index filter will reduce a walk of a table to only the defined indices to get 199 | # If one of the target OIDs is used in a lookup, the filter will apply ALL tables using this lookup 200 | # For a network switch, this could be used to collect a subset of interfaces such as uplinks 201 | # For a router, this could be used to collect all real ports but not vlans and other virtual interfaces 202 | # Specifying ifAlias or ifName if they are used in lookups with ifIndex will apply to the filter to 203 | # all the OIDs that depend on the lookup, such as ifSpeed, ifInHcOctets, etc. 204 | # This feature applies to any table(s) OIDs using a common index 205 | - targets: 206 | - bsnDot11EssSsid 207 | indices: ["2","3","4"] # List of interface indices to get 208 | 209 | dynamic: # dynamic filters are handed by the snmp exporter. The generator will simply pass on the configuration in the snmp.yml. 210 | # The exporter will do a snmp walk of the oid and will restrict snmp walk made on the targets 211 | # to the index matching the value in the values list. 212 | # This would be typically used to specify a filter for interfaces with a certain name in ifAlias, ifSpeed or admin status. 213 | # For example, only get interfaces that a gig and faster, or get interfaces that are named Up or interfaces that are admin Up 214 | - oid: 1.3.6.1.2.1.2.2.1.7 215 | targets: 216 | - "1.3.6.1.2.1.2.2.1.4" 217 | values: ["1", "2"] 218 | ``` 219 | 220 | ### EnumAsInfo and EnumAsStateSet 221 | 222 | SNMP contains the concept of integer indexed enumerations (enums). There are two ways 223 | to represent these strings in Prometheus. They can be "info" metrics, or they can be 224 | "state sets". SNMP does not specify which should be used, and it's up to the use case 225 | of the data. Some users may also prefer the raw integer value, rather than the string. 226 | 227 | In order to set enum integer to string mapping, you must use one of the two overrides. 228 | 229 | `EnumAsInfo` should be used for properties that provide inventory-like data. For example 230 | a device type, the name of a colour etc. It is important that this value is constant. 231 | 232 | `EnumAsStateSet` should be used for things that represent state or that you might want 233 | to alert on. For example the link state, is it up or down, is it in an error state, 234 | whether a panel is open or closed etc. Please be careful to not use this for high 235 | cardinality values as it will generate 1 time series per possible value. 236 | 237 | ## Where to get MIBs 238 | 239 | Some of these are quite sluggish, so use wget to download. 240 | 241 | Put the extracted mibs in a location NetSNMP can read them from. `$HOME/.snmp/mibs` is one option. 242 | 243 | * Cisco: ftp://ftp.cisco.com/pub/mibs/v2/v2.tar.gz 244 | * APC: https://download.schneider-electric.com/files?p_File_Name=powernet432.mib 245 | * Servertech: ftp://ftp.servertech.com/Pub/SNMP/sentry3/Sentry3.mib 246 | * Palo Alto PanOS 7.0 enterprise MIBs: https://www.paloaltonetworks.com/content/dam/pan/en_US/assets/zip/technical-documentation/snmp-mib-modules/PAN-MIB-MODULES-7.0.zip 247 | * Arista Networks: https://www.arista.com/assets/data/docs/MIBS/ARISTA-ENTITY-SENSOR-MIB.txt 248 | https://www.arista.com/assets/data/docs/MIBS/ARISTA-SW-IP-FORWARDING-MIB.txt 249 | https://www.arista.com/assets/data/docs/MIBS/ARISTA-SMI-MIB.txt 250 | * Synology: https://global.download.synology.com/download/Document/Software/DeveloperGuide/Firmware/DSM/All/enu/Synology_MIB_File.zip 251 | * MikroTik: http://download2.mikrotik.com/Mikrotik.mib 252 | * UCD-SNMP-MIB (Net-SNMP): http://www.net-snmp.org/docs/mibs/UCD-SNMP-MIB.txt 253 | * Ubiquiti Networks: http://dl.ubnt-ut.com/snmp/UBNT-MIB 254 | http://dl.ubnt-ut.com/snmp/UBNT-UniFi-MIB 255 | https://dl.ubnt.com/firmwares/airos-ubnt-mib/ubnt-mib.zip 256 | 257 | https://github.com/librenms/librenms/tree/master/mibs can also be a good source of MIBs. 258 | 259 | http://oidref.com is recommended for browsing MIBs. 260 | -------------------------------------------------------------------------------- /generator/config.go: -------------------------------------------------------------------------------- 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 | package main 15 | 16 | import ( 17 | "fmt" 18 | "strconv" 19 | 20 | "github.com/prometheus/snmp_exporter/config" 21 | ) 22 | 23 | // The generator config. 24 | type Config struct { 25 | Auths map[string]*config.Auth `yaml:"auths"` 26 | Modules map[string]*ModuleConfig `yaml:"modules"` 27 | Version int `yaml:"version,omitempty"` 28 | } 29 | 30 | type MetricOverrides struct { 31 | Ignore bool `yaml:"ignore,omitempty"` 32 | RegexpExtracts map[string][]config.RegexpExtract `yaml:"regex_extracts,omitempty"` 33 | DateTimePattern string `yaml:"datetime_pattern,omitempty"` 34 | Offset float64 `yaml:"offset,omitempty"` 35 | Scale float64 `yaml:"scale,omitempty"` 36 | Type string `yaml:"type,omitempty"` 37 | Help string `yaml:"help,omitempty"` 38 | Name string `yaml:"name,omitempty"` 39 | } 40 | 41 | // UnmarshalYAML implements the yaml.Unmarshaler interface. 42 | func (c *MetricOverrides) UnmarshalYAML(unmarshal func(interface{}) error) error { 43 | type plain MetricOverrides 44 | if err := unmarshal((*plain)(c)); err != nil { 45 | return err 46 | } 47 | // Ensure type for override is valid if one is defined. 48 | typ, ok := metricType(c.Type) 49 | if c.Type != "" && (!ok || typ != c.Type) { 50 | return fmt.Errorf("invalid metric type override '%s'", c.Type) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | type ModuleConfig struct { 57 | Walk []string `yaml:"walk"` 58 | Lookups []*Lookup `yaml:"lookups"` 59 | WalkParams config.WalkParams `yaml:",inline"` 60 | Overrides map[string]MetricOverrides `yaml:"overrides"` 61 | Filters config.Filters `yaml:"filters,omitempty"` 62 | } 63 | 64 | // UnmarshalYAML implements the yaml.Unmarshaler interface. 65 | func (c *ModuleConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 66 | type plain ModuleConfig 67 | if err := unmarshal((*plain)(c)); err != nil { 68 | return err 69 | } 70 | 71 | // Ensure indices in static filters are integer for input validation. 72 | for _, filter := range c.Filters.Static { 73 | for _, index := range filter.Indices { 74 | _, err := strconv.Atoi(index) 75 | if err != nil { 76 | return fmt.Errorf("invalid index '%s'. Index must be integer", index) 77 | } 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | type Lookup struct { 85 | SourceIndexes []string `yaml:"source_indexes"` 86 | Lookup string `yaml:"lookup"` 87 | DropSourceIndexes bool `yaml:"drop_source_indexes,omitempty"` 88 | } 89 | -------------------------------------------------------------------------------- /generator/main.go: -------------------------------------------------------------------------------- 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 | package main 15 | 16 | import ( 17 | "fmt" 18 | "log/slog" 19 | "os" 20 | "path/filepath" 21 | "regexp" 22 | "strings" 23 | 24 | "github.com/alecthomas/kingpin/v2" 25 | "github.com/prometheus/common/promslog" 26 | "github.com/prometheus/common/promslog/flag" 27 | "gopkg.in/yaml.v2" 28 | 29 | "github.com/prometheus/snmp_exporter/config" 30 | ) 31 | 32 | var ( 33 | cannotFindModuleRE = regexp.MustCompile(`Cannot find module \((.+)\): (.+)`) 34 | ) 35 | 36 | // Generate a snmp_exporter config and write it out. 37 | func generateConfig(nodes *Node, nameToNode map[string]*Node, logger *slog.Logger) error { 38 | outputPath, err := filepath.Abs(*outputPath) 39 | if err != nil { 40 | return fmt.Errorf("unable to determine absolute path for output") 41 | } 42 | 43 | content, err := os.ReadFile(*generatorYmlPath) 44 | if err != nil { 45 | return fmt.Errorf("error reading yml config: %s", err) 46 | } 47 | cfg := &Config{} 48 | err = yaml.UnmarshalStrict(content, cfg) 49 | if err != nil { 50 | return fmt.Errorf("error parsing yml config: %s", err) 51 | } 52 | 53 | outputConfig := config.Config{} 54 | outputConfig.Auths = cfg.Auths 55 | outputConfig.Modules = make(map[string]*config.Module, len(cfg.Modules)) 56 | for name, m := range cfg.Modules { 57 | logger.Info("Generating config for module", "module", name) 58 | // Give each module a copy of the tree so that it can be modified. 59 | mNodes := nodes.Copy() 60 | // Build the map with new pointers. 61 | mNameToNode := map[string]*Node{} 62 | walkNode(mNodes, func(n *Node) { 63 | mNameToNode[n.Oid] = n 64 | mNameToNode[n.Label] = n 65 | if n.Module != "" { 66 | mNameToNode[n.Module+"::"+n.Label] = n 67 | } 68 | }) 69 | out, err := generateConfigModule(m, mNodes, mNameToNode, logger) 70 | if err != nil { 71 | return err 72 | } 73 | outputConfig.Modules[name] = out 74 | outputConfig.Modules[name].WalkParams = m.WalkParams 75 | logger.Info("Generated metrics", "module", name, "metrics", len(outputConfig.Modules[name].Metrics)) 76 | } 77 | 78 | config.DoNotHideSecrets = true 79 | out, err := yaml.Marshal(outputConfig) 80 | config.DoNotHideSecrets = false 81 | if err != nil { 82 | return fmt.Errorf("error marshaling yml: %s", err) 83 | } 84 | 85 | // Check the generated config to catch auth/version issues. 86 | err = yaml.UnmarshalStrict(out, &config.Config{}) 87 | if err != nil { 88 | return fmt.Errorf("error parsing generated config: %s", err) 89 | } 90 | 91 | f, err := os.Create(outputPath) 92 | if err != nil { 93 | return fmt.Errorf("error opening output file: %s", err) 94 | } 95 | out = append([]byte("# WARNING: This file was auto-generated using snmp_exporter generator, manual changes will be lost.\n"), out...) 96 | _, err = f.Write(out) 97 | if err != nil { 98 | return fmt.Errorf("error writing to output file: %s", err) 99 | } 100 | logger.Info("Config written", "file", outputPath) 101 | return nil 102 | } 103 | 104 | var ( 105 | failOnParseErrors = kingpin.Flag("fail-on-parse-errors", "Exit with a non-zero status if there are MIB parsing errors").Default("true").Bool() 106 | snmpMIBOpts = kingpin.Flag("snmp.mibopts", "Toggle various defaults controlling MIB parsing, see snmpwalk --help").Default("eu").String() 107 | generateCommand = kingpin.Command("generate", "Generate snmp.yml from generator.yml") 108 | userMibsDir = kingpin.Flag("mibs-dir", "Paths to mibs directory").Default("").Short('m').Strings() 109 | generatorYmlPath = generateCommand.Flag("generator-path", "Path to the input generator.yml file").Default("generator.yml").Short('g').String() 110 | outputPath = generateCommand.Flag("output-path", "Path to write the snmp_exporter's config file").Default("snmp.yml").Short('o').String() 111 | parseErrorsCommand = kingpin.Command("parse_errors", "Debug: Print the parse errors output by NetSNMP") 112 | dumpCommand = kingpin.Command("dump", "Debug: Dump the parsed and prepared MIBs") 113 | ) 114 | 115 | func main() { 116 | promslogConfig := &promslog.Config{} 117 | flag.AddFlags(kingpin.CommandLine, promslogConfig) 118 | kingpin.HelpFlag.Short('h') 119 | command := kingpin.Parse() 120 | logger := promslog.New(promslogConfig) 121 | 122 | output, err := initSNMP(logger) 123 | if err != nil { 124 | logger.Error("Error initializing netsnmp", "err", err) 125 | os.Exit(1) 126 | } 127 | 128 | parseOutput := scanParseOutput(logger, output) 129 | parseErrors := len(parseOutput) 130 | 131 | nodes := getMIBTree() 132 | nameToNode := prepareTree(nodes, logger) 133 | 134 | switch command { 135 | case generateCommand.FullCommand(): 136 | if *failOnParseErrors && parseErrors > 0 { 137 | logger.Error("Failing on reported parse error(s)", "help", "Use 'generator parse_errors' command to see errors, --no-fail-on-parse-errors to ignore") 138 | } else { 139 | err := generateConfig(nodes, nameToNode, logger) 140 | if err != nil { 141 | logger.Error("Error generating config netsnmp", "err", err) 142 | os.Exit(1) 143 | } 144 | } 145 | case parseErrorsCommand.FullCommand(): 146 | if parseErrors > 0 { 147 | fmt.Printf("%s\n", strings.Join(parseOutput, "\n")) 148 | } else { 149 | logger.Info("No parse errors") 150 | } 151 | case dumpCommand.FullCommand(): 152 | walkNode(nodes, func(n *Node) { 153 | t := n.Type 154 | if n.FixedSize != 0 { 155 | t = fmt.Sprintf("%s(%d)", n.Type, n.FixedSize) 156 | } 157 | implied := "" 158 | if n.ImpliedIndex { 159 | implied = "(implied)" 160 | } 161 | fmt.Printf("%s %s %s %q %q %s%s %v %s\n", 162 | n.Oid, n.Label, t, n.TextualConvention, n.Hint, n.Indexes, implied, n.EnumValues, n.Description) 163 | }) 164 | } 165 | if *failOnParseErrors && parseErrors > 0 { 166 | os.Exit(1) 167 | } 168 | } 169 | 170 | func scanParseOutput(logger *slog.Logger, output string) []string { 171 | var parseOutput []string 172 | output = strings.TrimSpace(strings.ToValidUTF8(output, "�")) 173 | if len(output) > 0 { 174 | parseOutput = strings.Split(output, "\n") 175 | } 176 | parseErrors := len(parseOutput) 177 | 178 | if parseErrors > 0 { 179 | logger.Warn("NetSNMP reported parse error(s)", "errors", parseErrors) 180 | } 181 | 182 | for _, line := range parseOutput { 183 | if strings.HasPrefix(line, "Cannot find module") { 184 | missing := cannotFindModuleRE.FindStringSubmatch(line) 185 | logger.Error("Missing MIB", "mib", missing[1], "from", missing[2]) 186 | } 187 | } 188 | return parseOutput 189 | } 190 | -------------------------------------------------------------------------------- /generator/mibs/.do_not_remove: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prometheus/snmp_exporter/2fc7f4ffd5cad7653db3028de39a252f06e29736/generator/mibs/.do_not_remove -------------------------------------------------------------------------------- /generator/net_snmp.go: -------------------------------------------------------------------------------- 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 | package main 15 | 16 | /* 17 | #cgo LDFLAGS: -lnetsnmp -L/usr/local/lib 18 | #cgo CFLAGS: -I/usr/local/include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | // From parse.c 26 | // Hacky workarounds to detect which version of net-snmp this 27 | // based on various headers that appear in 5.8 and 5.9. 28 | #if !defined(NETSNMP_DS_LIB_ADD_FORWARDER_INFO) 29 | #if defined(SNMPD_CALLBACK_UNREGISTER_NOTIFICATIONS) 30 | #define MAXTC 16384 31 | #else 32 | #define MAXTC 4096 33 | #endif 34 | #endif 35 | 36 | struct tc { 37 | int type; 38 | int modid; 39 | char *descriptor; 40 | char *hint; 41 | struct enum_list *enums; 42 | struct range_list *ranges; 43 | char *description; 44 | #if !defined(NETSNMP_DS_LIB_ADD_FORWARDER_INFO) 45 | } tclist[MAXTC]; 46 | int tc_alloc = MAXTC; 47 | #else 48 | } *tclist; 49 | int tc_alloc; 50 | #endif 51 | 52 | // Return the size of a fixed, or 0 if it is not fixed. 53 | int get_tc_fixed_size(int tc_index) { 54 | if (tc_index < 0 || tc_index >= tc_alloc) { 55 | return 0; 56 | } 57 | struct range_list *ranges; 58 | ranges = tclist[tc_index].ranges; 59 | // Look for one range with only one possible value. 60 | if (ranges == NULL || ranges->low != ranges->high || ranges->next != NULL) { 61 | return 0; 62 | } 63 | return ranges->low; 64 | } 65 | 66 | */ 67 | import "C" 68 | 69 | import ( 70 | "fmt" 71 | "io" 72 | "log/slog" 73 | "os" 74 | "sort" 75 | "strings" 76 | ) 77 | 78 | // One entry in the tree of the MIB. 79 | type Node struct { 80 | Oid string 81 | subid int64 82 | Module string 83 | Label string 84 | Augments string 85 | Children []*Node 86 | Description string 87 | Type string 88 | Hint string 89 | TextualConvention string 90 | FixedSize int 91 | Units string 92 | Access string 93 | EnumValues map[int]string 94 | 95 | Indexes []string 96 | ImpliedIndex bool 97 | } 98 | 99 | // Copy returns a deep copy of the tree underneath the current Node. 100 | func (n *Node) Copy() *Node { 101 | newNode := *n 102 | newNode.Children = make([]*Node, 0, len(n.Children)) 103 | newNode.EnumValues = make(map[int]string, len(n.EnumValues)) 104 | newNode.Indexes = make([]string, len(n.Indexes)) 105 | copy(newNode.Indexes, n.Indexes) 106 | // Deep copy children and enums. 107 | for _, child := range n.Children { 108 | newNode.Children = append(newNode.Children, child.Copy()) 109 | } 110 | for k, v := range n.EnumValues { 111 | newNode.EnumValues[k] = v 112 | } 113 | return &newNode 114 | } 115 | 116 | // Adapted from parse.h. 117 | var ( 118 | netSnmptypeMap = map[int]string{ 119 | 0: "OTHER", 120 | 1: "OBJID", 121 | 2: "OCTETSTR", 122 | 3: "INTEGER", 123 | 4: "NETADDR", 124 | 5: "IPADDR", 125 | 6: "COUNTER", 126 | 7: "GAUGE", 127 | 8: "TIMETICKS", 128 | 9: "OPAQUE", 129 | 10: "NULL", 130 | 11: "COUNTER64", 131 | 12: "BITSTRING", 132 | 13: "NSAPADDRESS", 133 | 14: "UINTEGER", 134 | 15: "UNSIGNED32", 135 | 16: "INTEGER32", 136 | 20: "TRAPTYPE", 137 | 21: "NOTIFTYPE", 138 | 22: "OBJGROUP", 139 | 23: "NOTIFGROUP", 140 | 24: "MODID", 141 | 25: "AGENTCAP", 142 | 26: "MODCOMP", 143 | 27: "OBJIDENTITY", 144 | } 145 | netSnmpaccessMap = map[int]string{ 146 | 18: "ACCESS_READONLY", 147 | 19: "ACCESS_READWRITE", 148 | 20: "ACCESS_WRITEONLY", 149 | 21: "ACCESS_NOACCESS", 150 | 67: "ACCESS_NOTIFY", 151 | 48: "ACCESS_CREATE", 152 | } 153 | ) 154 | 155 | // getMibsDir joins the user-specified MIB directories into a single string; if the user didn't pass any, 156 | // the default netsnmp mibs directory is returned. 157 | func getMibsDir(paths []string) string { 158 | if len(paths) == 1 && paths[0] == "" { 159 | return C.GoString(C.netsnmp_get_mib_directory()) 160 | } 161 | return strings.Join(paths, ":") 162 | } 163 | 164 | // Initialize NetSNMP. Returns MIB parse errors. 165 | // 166 | // Warning: This function plays with the stderr file descriptor. 167 | func initSNMP(logger *slog.Logger) (string, error) { 168 | // Load all the MIBs. 169 | err := os.Setenv("MIBS", "ALL") 170 | if err != nil { 171 | return "", err 172 | } 173 | mibsDir := getMibsDir(*userMibsDir) 174 | logger.Info("Loading MIBs", "from", mibsDir) 175 | C.netsnmp_set_mib_directory(C.CString(mibsDir)) 176 | if *snmpMIBOpts != "" { 177 | C.snmp_mib_toggle_options(C.CString(*snmpMIBOpts)) 178 | } 179 | // We want the descriptions. 180 | C.snmp_set_save_descriptions(1) 181 | // Make stderr go to a pipe, as netsnmp tends to spew a 182 | // lot of errors on startup that there's no apparent 183 | // way to disable or redirect. 184 | r, w, err := os.Pipe() 185 | if err != nil { 186 | return "", fmt.Errorf("error creating pipe: %s", err) 187 | } 188 | defer r.Close() 189 | defer w.Close() 190 | savedStderrFd := C.dup(2) 191 | C.close(2) 192 | C.dup2(C.int(w.Fd()), 2) 193 | ch := make(chan string) 194 | errch := make(chan error) 195 | go func() { 196 | data, err := io.ReadAll(r) 197 | if err != nil { 198 | errch <- fmt.Errorf("error reading from pipe: %s", err) 199 | return 200 | } 201 | errch <- nil 202 | ch <- string(data) 203 | }() 204 | 205 | // Do the initialization. 206 | C.netsnmp_init_mib() 207 | 208 | // Restore stderr to normal. 209 | w.Close() 210 | C.close(2) 211 | C.dup2(savedStderrFd, 2) 212 | C.close(savedStderrFd) 213 | if err := <-errch; err != nil { 214 | return "", err 215 | } 216 | return <-ch, nil 217 | } 218 | 219 | // Walk NetSNMP MIB tree, building a Go tree from it. 220 | func buildMIBTree(t *C.struct_tree, n *Node, oid string) { 221 | n.subid = int64(t.subid) 222 | if oid != "" { 223 | n.Oid = fmt.Sprintf("%s.%d", oid, t.subid) 224 | } else { 225 | n.Oid = fmt.Sprintf("%d", t.subid) 226 | } 227 | if m := C.find_module(t.modid); m != nil { 228 | n.Module = C.GoString(m.name) 229 | } 230 | n.Label = C.GoString(t.label) 231 | if typ, ok := netSnmptypeMap[int(t._type)]; ok { 232 | n.Type = typ 233 | } else { 234 | n.Type = "unknown" 235 | } 236 | 237 | if access, ok := netSnmpaccessMap[int(t.access)]; ok { 238 | n.Access = access 239 | } else { 240 | n.Access = "unknown" 241 | } 242 | 243 | n.Augments = C.GoString(t.augments) 244 | n.Description = C.GoString(t.description) 245 | n.Hint = C.GoString(t.hint) 246 | n.TextualConvention = C.GoString(C.get_tc_descriptor(t.tc_index)) 247 | n.FixedSize = int(C.get_tc_fixed_size(t.tc_index)) 248 | n.Units = C.GoString(t.units) 249 | 250 | n.EnumValues = map[int]string{} 251 | enum := t.enums 252 | for enum != nil { 253 | n.EnumValues[int(enum.value)] = C.GoString(enum.label) 254 | enum = enum.next 255 | } 256 | 257 | if t.child_list == nil { 258 | return 259 | } 260 | 261 | head := t.child_list 262 | n.Children = []*Node{} 263 | for head != nil { 264 | child := &Node{} 265 | // Prepend, as nodes are backwards. 266 | n.Children = append([]*Node{child}, n.Children...) 267 | buildMIBTree(head, child, n.Oid) 268 | head = head.next_peer 269 | } 270 | 271 | // Ensure things are consistently ordered. 272 | sort.Slice(n.Children, func(i, j int) bool { 273 | return n.Children[i].subid < n.Children[j].subid 274 | }) 275 | 276 | // Set names of indexes on each child. 277 | // In practice this means only the entry will have it. 278 | index := t.indexes 279 | indexes := []string{} 280 | for index != nil { 281 | indexes = append(indexes, C.GoString(index.ilabel)) 282 | if index.isimplied != 0 { 283 | n.ImpliedIndex = true 284 | } 285 | index = index.next 286 | } 287 | n.Indexes = indexes 288 | } 289 | 290 | // Convert the NetSNMP MIB tree to a Go data structure. 291 | func getMIBTree() *Node { 292 | 293 | tree := C.get_tree_head() 294 | head := &Node{} 295 | buildMIBTree(tree, head, "") 296 | return head 297 | } 298 | -------------------------------------------------------------------------------- /generator/tree.go: -------------------------------------------------------------------------------- 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 | package main 15 | 16 | import ( 17 | "fmt" 18 | "log/slog" 19 | "regexp" 20 | "sort" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/prometheus/snmp_exporter/config" 25 | ) 26 | 27 | // These types have one following the other. 28 | // We need to check indexes and sequences have them 29 | // in the right order, so the exporter can handle them. 30 | var combinedTypes = map[string]string{ 31 | "InetAddress": "InetAddressType", 32 | "InetAddressMissingSize": "InetAddressType", 33 | "LldpPortId": "LldpPortIdSubtype", 34 | } 35 | 36 | // Helper to walk MIB nodes. 37 | func walkNode(n *Node, f func(n *Node)) { 38 | f(n) 39 | for _, c := range n.Children { 40 | walkNode(c, f) 41 | } 42 | } 43 | 44 | // Transform the tree. 45 | func prepareTree(nodes *Node, logger *slog.Logger) map[string]*Node { 46 | // Build a map from names and oids to nodes. 47 | nameToNode := map[string]*Node{} 48 | walkNode(nodes, func(n *Node) { 49 | nameToNode[n.Oid] = n 50 | nameToNode[n.Label] = n 51 | }) 52 | 53 | // Trim down description to first sentence, removing extra whitespace. 54 | walkNode(nodes, func(n *Node) { 55 | s := strings.Join(strings.Fields(n.Description), " ") 56 | n.Description = strings.Split(s, ". ")[0] 57 | }) 58 | 59 | // Fix indexes to "INTEGER" rather than an object name. 60 | // Example: snSlotsEntry in LANOPTICS-HUB-MIB. 61 | walkNode(nodes, func(n *Node) { 62 | indexes := []string{} 63 | for _, i := range n.Indexes { 64 | if i == "INTEGER" { 65 | // Use the TableEntry name. 66 | indexes = append(indexes, n.Label) 67 | } else { 68 | indexes = append(indexes, i) 69 | } 70 | } 71 | n.Indexes = indexes 72 | }) 73 | 74 | // Copy over indexes based on augments. 75 | walkNode(nodes, func(n *Node) { 76 | if n.Augments == "" { 77 | return 78 | } 79 | augmented, ok := nameToNode[n.Augments] 80 | if !ok { 81 | logger.Warn("Can't find augmenting node", "augments", n.Augments, "node", n.Label) 82 | return 83 | } 84 | for _, c := range n.Children { 85 | c.Indexes = augmented.Indexes 86 | c.ImpliedIndex = augmented.ImpliedIndex 87 | } 88 | n.Indexes = augmented.Indexes 89 | n.ImpliedIndex = augmented.ImpliedIndex 90 | }) 91 | 92 | // Copy indexes from table entries down to the entries. 93 | walkNode(nodes, func(n *Node) { 94 | if len(n.Indexes) != 0 { 95 | for _, c := range n.Children { 96 | c.Indexes = n.Indexes 97 | c.ImpliedIndex = n.ImpliedIndex 98 | } 99 | } 100 | }) 101 | 102 | // Include both ASCII and UTF-8 in DisplayString, even though DisplayString 103 | // is technically only ASCII. 104 | displayStringRe := regexp.MustCompile(`^\d+[at]$`) 105 | 106 | // Apply various tweaks to the types. 107 | walkNode(nodes, func(n *Node) { 108 | // Set type on MAC addresses and strings. 109 | // RFC 2579 110 | switch n.Hint { 111 | case "1x:": 112 | n.Type = "PhysAddress48" 113 | } 114 | if displayStringRe.MatchString(n.Hint) { 115 | n.Type = "DisplayString" 116 | } 117 | 118 | // Some MIBs refer to RFC1213 for this, which is too 119 | // old to have the right hint set. 120 | if n.TextualConvention == "DisplayString" { 121 | n.Type = "DisplayString" 122 | } 123 | if n.TextualConvention == "PhysAddress" { 124 | n.Type = "PhysAddress48" 125 | } 126 | 127 | // Promote Opaque Float/Double textual convention to type. 128 | if n.TextualConvention == "Float" || n.TextualConvention == "Double" { 129 | n.Type = n.TextualConvention 130 | } 131 | 132 | // Convert RFC 2579 DateAndTime textual convention to type. 133 | if n.TextualConvention == "DateAndTime" { 134 | n.Type = "DateAndTime" 135 | } 136 | if n.TextualConvention == "ParseDateAndTime" { 137 | n.Type = "ParseDateAndTime" 138 | } 139 | if n.TextualConvention == "NTPTimeStamp" { 140 | n.Type = "NTPTimeStamp" 141 | } 142 | // Convert RFC 4001 InetAddress types textual convention to type. 143 | if n.TextualConvention == "InetAddressIPv4" || n.TextualConvention == "InetAddressIPv6" || n.TextualConvention == "InetAddress" { 144 | n.Type = n.TextualConvention 145 | } 146 | // Convert LLDP-MIB LldpPortId type textual convention to type. 147 | if n.TextualConvention == "LldpPortId" { 148 | n.Type = n.TextualConvention 149 | } 150 | }) 151 | 152 | return nameToNode 153 | } 154 | 155 | func metricType(t string) (string, bool) { 156 | if _, ok := combinedTypes[t]; ok { 157 | return t, true 158 | } 159 | switch t { 160 | case "gauge", "INTEGER", "GAUGE", "TIMETICKS", "UINTEGER", "UNSIGNED32", "INTEGER32": 161 | return "gauge", true 162 | case "counter", "COUNTER", "COUNTER64": 163 | return "counter", true 164 | case "OctetString", "OCTETSTR", "OBJID": 165 | return "OctetString", true 166 | case "BITSTRING": 167 | return "Bits", true 168 | case "InetAddressIPv4", "IpAddr", "IPADDR", "NETADDR": 169 | return "InetAddressIPv4", true 170 | case "PhysAddress48", "DisplayString", "Float", "Double", "InetAddressIPv6": 171 | return t, true 172 | case "DateAndTime": 173 | return t, true 174 | case "ParseDateAndTime": 175 | return t, true 176 | case "NTPTimeStamp": 177 | return t, true 178 | case "EnumAsInfo", "EnumAsStateSet": 179 | return t, true 180 | default: 181 | // Unsupported type. 182 | return "", false 183 | } 184 | } 185 | 186 | func metricAccess(a string) bool { 187 | switch a { 188 | case "ACCESS_READONLY", "ACCESS_READWRITE", "ACCESS_CREATE", "ACCESS_NOACCESS": 189 | return true 190 | default: 191 | // The others are inaccessible metrics. 192 | return false 193 | } 194 | } 195 | 196 | // Reduce a set of overlapping OID subtrees. 197 | func minimizeOids(oids []string) []string { 198 | sort.Strings(oids) 199 | prevOid := "" 200 | minimized := []string{} 201 | for _, oid := range oids { 202 | if !strings.HasPrefix(oid+".", prevOid) || prevOid == "" { 203 | minimized = append(minimized, oid) 204 | prevOid = oid + "." 205 | } 206 | } 207 | return minimized 208 | } 209 | 210 | // Search node tree for the longest OID match. 211 | func searchNodeTree(oid string, node *Node) *Node { 212 | if node == nil || !strings.HasPrefix(oid+".", node.Oid+".") { 213 | return nil 214 | } 215 | 216 | for _, child := range node.Children { 217 | match := searchNodeTree(oid, child) 218 | if match != nil { 219 | return match 220 | } 221 | } 222 | return node 223 | } 224 | 225 | type oidMetricType uint8 226 | 227 | const ( 228 | oidNotFound oidMetricType = iota 229 | oidScalar 230 | oidInstance 231 | oidSubtree 232 | ) 233 | 234 | // Find node in SNMP MIB tree that represents the metric. 235 | func getMetricNode(oid string, node *Node, nameToNode map[string]*Node) (*Node, oidMetricType) { 236 | // Check if is a known OID/name. 237 | n, ok := nameToNode[oid] 238 | if ok { 239 | // Known node, check if OID is a valid metric or a subtree. 240 | _, ok = metricType(n.Type) 241 | if ok && metricAccess(n.Access) && len(n.Indexes) == 0 { 242 | return n, oidScalar 243 | } 244 | return n, oidSubtree 245 | } 246 | 247 | // Unknown OID/name, search Node tree for longest match. 248 | n = searchNodeTree(oid, node) 249 | if n == nil { 250 | return nil, oidNotFound 251 | } 252 | 253 | // Table instances must be a valid metric node and have an index. 254 | _, ok = metricType(n.Type) 255 | ok = ok && metricAccess(n.Access) 256 | if !ok || len(n.Indexes) == 0 { 257 | return nil, oidNotFound 258 | } 259 | return n, oidInstance 260 | } 261 | 262 | // In the case of multiple nodes with the same label try to return the node 263 | // where the OID matches in every branch apart from the last one. 264 | func getIndexNode(lookup string, nameToNode map[string]*Node, metricOid string) *Node { 265 | for _, node := range nameToNode { 266 | if node.Label != lookup { 267 | continue 268 | } 269 | 270 | oid := strings.Split(metricOid, ".") 271 | oidPrefix := strings.Join(oid[:len(oid)-1], ".") 272 | 273 | if strings.HasPrefix(node.Oid, oidPrefix) { 274 | return node 275 | } 276 | } 277 | 278 | // If no node matches, revert to previous behavior. 279 | return nameToNode[lookup] 280 | } 281 | 282 | func generateConfigModule(cfg *ModuleConfig, node *Node, nameToNode map[string]*Node, logger *slog.Logger) (*config.Module, error) { 283 | out := &config.Module{} 284 | needToWalk := map[string]struct{}{} 285 | tableInstances := map[string][]string{} 286 | 287 | // Apply type overrides for the current module. 288 | for name, params := range cfg.Overrides { 289 | if params.Type == "" { 290 | continue 291 | } 292 | // Find node to override. 293 | n, ok := nameToNode[name] 294 | if !ok { 295 | logger.Warn("Could not find node to override type", "node", name) 296 | continue 297 | } 298 | // params.Type validated at generator configuration. 299 | n.Type = params.Type 300 | } 301 | 302 | // Remove redundant OIDs to be walked. 303 | toWalk := []string{} 304 | for _, oid := range cfg.Walk { 305 | if strings.HasPrefix(oid, ".") { 306 | return nil, fmt.Errorf("invalid OID %s, prefix of '.' should be removed", oid) 307 | } 308 | // Resolve name to OID if possible. 309 | n, ok := nameToNode[oid] 310 | if ok { 311 | toWalk = append(toWalk, n.Oid) 312 | } else { 313 | toWalk = append(toWalk, oid) 314 | } 315 | } 316 | toWalk = minimizeOids(toWalk) 317 | 318 | // Find all top-level nodes. 319 | metricNodes := map[*Node]struct{}{} 320 | for _, oid := range toWalk { 321 | metricNode, oidType := getMetricNode(oid, node, nameToNode) 322 | switch oidType { 323 | case oidNotFound: 324 | return nil, fmt.Errorf("cannot find oid '%s' to walk", oid) 325 | case oidSubtree: 326 | needToWalk[oid] = struct{}{} 327 | case oidInstance: 328 | // Add a trailing period to the OID to indicate a "Get" instead of a "Walk". 329 | needToWalk[oid+"."] = struct{}{} 330 | // Save instance index for lookup. 331 | index := strings.Replace(oid, metricNode.Oid, "", 1) 332 | tableInstances[metricNode.Oid] = append(tableInstances[metricNode.Oid], index) 333 | case oidScalar: 334 | // Scalar OIDs must be accessed using index 0. 335 | needToWalk[oid+".0."] = struct{}{} 336 | } 337 | metricNodes[metricNode] = struct{}{} 338 | } 339 | // Sort the metrics by OID to make the output deterministic. 340 | metrics := make([]*Node, 0, len(metricNodes)) 341 | for key := range metricNodes { 342 | metrics = append(metrics, key) 343 | } 344 | sort.Slice(metrics, func(i, j int) bool { 345 | return metrics[i].Oid < metrics[j].Oid 346 | }) 347 | 348 | // Find all the usable metrics. 349 | for _, metricNode := range metrics { 350 | walkNode(metricNode, func(n *Node) { 351 | t, ok := metricType(n.Type) 352 | if !ok { 353 | return // Unsupported type. 354 | } 355 | 356 | if !metricAccess(n.Access) { 357 | return // Inaccessible metrics. 358 | } 359 | 360 | metric := &config.Metric{ 361 | Name: sanitizeLabelName(n.Label), 362 | Oid: n.Oid, 363 | Type: t, 364 | Help: n.Description + " - " + n.Oid, 365 | Indexes: []*config.Index{}, 366 | Lookups: []*config.Lookup{}, 367 | EnumValues: n.EnumValues, 368 | } 369 | 370 | if cfg.Overrides[metric.Name].Ignore { 371 | return // Ignored metric. 372 | } 373 | 374 | // Afi (Address family) 375 | prevType := "" 376 | // Safi (Subsequent address family, e.g. Multicast/Unicast) 377 | prev2Type := "" 378 | for count, i := range n.Indexes { 379 | index := &config.Index{Labelname: i} 380 | indexNode, ok := nameToNode[i] 381 | if !ok { 382 | logger.Warn("Could not find index for node", "node", n.Label, "index", i) 383 | return 384 | } 385 | index.Type, ok = metricType(indexNode.Type) 386 | if !ok { 387 | logger.Warn("Can't handle index type on node", "node", n.Label, "index", i, "type", indexNode.Type) 388 | return 389 | } 390 | index.FixedSize = indexNode.FixedSize 391 | if n.ImpliedIndex && count+1 == len(n.Indexes) { 392 | index.Implied = true 393 | } 394 | index.EnumValues = indexNode.EnumValues 395 | 396 | // Convert (InetAddressType,InetAddress) to (InetAddress) 397 | if subtype, ok := combinedTypes[index.Type]; ok { 398 | if prevType == subtype { 399 | metric.Indexes = metric.Indexes[:len(metric.Indexes)-1] 400 | } else if prev2Type == subtype { 401 | metric.Indexes = metric.Indexes[:len(metric.Indexes)-2] 402 | } else { 403 | logger.Warn("Can't handle index type on node, missing preceding", "node", n.Label, "type", index.Type, "missing", subtype) 404 | return 405 | } 406 | } 407 | prev2Type = prevType 408 | prevType = indexNode.TextualConvention 409 | metric.Indexes = append(metric.Indexes, index) 410 | } 411 | out.Metrics = append(out.Metrics, metric) 412 | }) 413 | } 414 | 415 | // Build an map of all oid targeted by a filter to access it easily later. 416 | filterMap := map[string][]string{} 417 | 418 | for _, filter := range cfg.Filters.Static { 419 | for _, oid := range filter.Targets { 420 | n, ok := nameToNode[oid] 421 | if ok { 422 | oid = n.Oid 423 | } 424 | filterMap[oid] = filter.Indices 425 | } 426 | } 427 | 428 | // Apply lookups. 429 | for _, metric := range out.Metrics { 430 | toDelete := []string{} 431 | 432 | // Build a list of lookup labels which are required as index. 433 | requiredAsIndex := []string{} 434 | for _, lookup := range cfg.Lookups { 435 | requiredAsIndex = append(requiredAsIndex, lookup.SourceIndexes...) 436 | } 437 | 438 | for _, lookup := range cfg.Lookups { 439 | foundIndexes := 0 440 | // See if all lookup indexes are present. 441 | for _, index := range metric.Indexes { 442 | for _, lookupIndex := range lookup.SourceIndexes { 443 | if index.Labelname == lookupIndex { 444 | foundIndexes++ 445 | } 446 | } 447 | } 448 | if foundIndexes == len(lookup.SourceIndexes) { 449 | if _, ok := nameToNode[lookup.Lookup]; !ok { 450 | return nil, fmt.Errorf("unknown index '%s'", lookup.Lookup) 451 | } 452 | indexNode := getIndexNode(lookup.Lookup, nameToNode, metric.Oid) 453 | typ, ok := metricType(indexNode.Type) 454 | if !ok { 455 | return nil, fmt.Errorf("unknown index type %s for %s", indexNode.Type, lookup.Lookup) 456 | } 457 | l := &config.Lookup{ 458 | Labelname: sanitizeLabelName(indexNode.Label), 459 | Type: typ, 460 | Oid: indexNode.Oid, 461 | } 462 | for _, oldIndex := range lookup.SourceIndexes { 463 | l.Labels = append(l.Labels, sanitizeLabelName(oldIndex)) 464 | } 465 | metric.Lookups = append(metric.Lookups, l) 466 | 467 | // If lookup label is used as source index in another lookup, 468 | // we need to add this new label as another index. 469 | for _, sourceIndex := range requiredAsIndex { 470 | if sourceIndex == l.Labelname { 471 | idx := &config.Index{Labelname: l.Labelname, Type: l.Type} 472 | metric.Indexes = append(metric.Indexes, idx) 473 | break 474 | } 475 | } 476 | 477 | // Make sure we walk the lookup OID(s). 478 | if len(tableInstances[metric.Oid]) > 0 { 479 | for _, index := range tableInstances[metric.Oid] { 480 | needToWalk[indexNode.Oid+index+"."] = struct{}{} 481 | } 482 | } else { 483 | needToWalk[indexNode.Oid] = struct{}{} 484 | } 485 | // We apply the same filter to metric.Oid if the lookup oid is filtered. 486 | indices, found := filterMap[indexNode.Oid] 487 | if found { 488 | delete(needToWalk, metric.Oid) 489 | for _, index := range indices { 490 | needToWalk[metric.Oid+"."+index+"."] = struct{}{} 491 | } 492 | } 493 | if lookup.DropSourceIndexes { 494 | // Avoid leaving the old labelname around. 495 | toDelete = append(toDelete, lookup.SourceIndexes...) 496 | } 497 | } 498 | } 499 | for _, l := range toDelete { 500 | metric.Lookups = append(metric.Lookups, &config.Lookup{ 501 | Labelname: sanitizeLabelName(l), 502 | }) 503 | } 504 | } 505 | 506 | // Ensure index label names are sane. 507 | for _, metric := range out.Metrics { 508 | for _, index := range metric.Indexes { 509 | index.Labelname = sanitizeLabelName(index.Labelname) 510 | } 511 | } 512 | 513 | // Check that the object before an InetAddress is an InetAddressType. 514 | // If not, change it to an OctetString. 515 | for _, metric := range out.Metrics { 516 | if metric.Type == "InetAddress" || metric.Type == "InetAddressMissingSize" { 517 | // Get previous oid. 518 | oids := strings.Split(metric.Oid, ".") 519 | i, _ := strconv.Atoi(oids[len(oids)-1]) 520 | oids[len(oids)-1] = strconv.Itoa(i - 1) 521 | prevOid := strings.Join(oids, ".") 522 | if prevObj, ok := nameToNode[prevOid]; !ok || prevObj.TextualConvention != "InetAddressType" { 523 | metric.Type = "OctetString" 524 | } else { 525 | // Make sure the InetAddressType is included. 526 | if len(tableInstances[metric.Oid]) > 0 { 527 | for _, index := range tableInstances[metric.Oid] { 528 | needToWalk[prevOid+index+"."] = struct{}{} 529 | } 530 | } else { 531 | needToWalk[prevOid] = struct{}{} 532 | } 533 | } 534 | } 535 | } 536 | 537 | // Apply module config overrides to their corresponding metrics. 538 | for name, params := range cfg.Overrides { 539 | for _, metric := range out.Metrics { 540 | if name == metric.Name || name == metric.Oid { 541 | metric.RegexpExtracts = params.RegexpExtracts 542 | metric.DateTimePattern = params.DateTimePattern 543 | metric.Offset = params.Offset 544 | metric.Scale = params.Scale 545 | if params.Help != "" { 546 | metric.Help = params.Help 547 | } 548 | if params.Name != "" { 549 | metric.Name = params.Name 550 | } 551 | } 552 | } 553 | } 554 | 555 | // Apply filters. 556 | for _, filter := range cfg.Filters.Static { 557 | // Delete the oid targeted by the filter, as we won't walk the whole table. 558 | for _, oid := range filter.Targets { 559 | n, ok := nameToNode[oid] 560 | if ok { 561 | oid = n.Oid 562 | } 563 | delete(needToWalk, oid) 564 | for _, index := range filter.Indices { 565 | needToWalk[oid+"."+index+"."] = struct{}{} 566 | } 567 | } 568 | } 569 | 570 | out.Filters = cfg.Filters.Dynamic 571 | 572 | oids := []string{} 573 | for k := range needToWalk { 574 | oids = append(oids, k) 575 | } 576 | // Remove redundant OIDs and separate Walk and Get OIDs. 577 | for _, k := range minimizeOids(oids) { 578 | if k[len(k)-1:] == "." { 579 | out.Get = append(out.Get, k[:len(k)-1]) 580 | } else { 581 | out.Walk = append(out.Walk, k) 582 | } 583 | } 584 | return out, nil 585 | } 586 | 587 | var ( 588 | invalidLabelCharRE = regexp.MustCompile(`[^a-zA-Z0-9_]`) 589 | ) 590 | 591 | func sanitizeLabelName(name string) string { 592 | return invalidLabelCharRE.ReplaceAllString(name, "_") 593 | } 594 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prometheus/snmp_exporter 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/alecthomas/kingpin/v2 v2.4.0 9 | github.com/gosnmp/gosnmp v1.40.0 10 | github.com/itchyny/timefmt-go v0.1.6 11 | github.com/prometheus/client_golang v1.22.0 12 | github.com/prometheus/client_model v0.6.2 13 | github.com/prometheus/common v0.64.0 14 | github.com/prometheus/exporter-toolkit v0.14.0 15 | gopkg.in/yaml.v2 v2.4.0 16 | ) 17 | 18 | require ( 19 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 23 | github.com/jpillora/backoff v1.0.0 // indirect 24 | github.com/mdlayher/socket v0.4.1 // indirect 25 | github.com/mdlayher/vsock v1.2.1 // indirect 26 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 27 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 28 | github.com/prometheus/procfs v0.15.1 // indirect 29 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 30 | golang.org/x/crypto v0.38.0 // indirect 31 | golang.org/x/net v0.40.0 // indirect 32 | golang.org/x/oauth2 v0.30.0 // indirect 33 | golang.org/x/sync v0.14.0 // indirect 34 | golang.org/x/sys v0.33.0 // indirect 35 | golang.org/x/text v0.25.0 // indirect 36 | google.golang.org/protobuf v1.36.6 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= 2 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 3 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 4 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 5 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 6 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 10 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 15 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 16 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 17 | github.com/gosnmp/gosnmp v1.40.0 h1:MvSqHZaNnhMKdn5IVhyYzCsVfXV1lgg6ZgLRku7FVcM= 18 | github.com/gosnmp/gosnmp v1.40.0/go.mod h1:CxVS6bXqmWZlafUj9pZUnQX5e4fAltqPcijxWpCitDo= 19 | github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= 20 | github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= 21 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 22 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 23 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 24 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 25 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 26 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 27 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 28 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 29 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 30 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 31 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 32 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 33 | github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= 34 | github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= 35 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 37 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 38 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 39 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 42 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 43 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 44 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 45 | github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 46 | github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 47 | github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg= 48 | github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA= 49 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 50 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 51 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 52 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 54 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 55 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 56 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 57 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 58 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 59 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 60 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 61 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 62 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 63 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 64 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 65 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 66 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 67 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 68 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 69 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 70 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 71 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 72 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 75 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 76 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 77 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 78 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 79 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 80 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 81 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 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 | package main 15 | 16 | import ( 17 | "fmt" 18 | "log/slog" 19 | "net/http" 20 | _ "net/http/pprof" 21 | "os" 22 | "os/signal" 23 | "strings" 24 | "sync" 25 | "syscall" 26 | 27 | "github.com/alecthomas/kingpin/v2" 28 | "github.com/prometheus/client_golang/prometheus" 29 | versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" 30 | "github.com/prometheus/client_golang/prometheus/promauto" 31 | "github.com/prometheus/client_golang/prometheus/promhttp" 32 | "github.com/prometheus/common/promslog" 33 | "github.com/prometheus/common/promslog/flag" 34 | "github.com/prometheus/common/version" 35 | "github.com/prometheus/exporter-toolkit/web" 36 | webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" 37 | yaml "gopkg.in/yaml.v2" 38 | 39 | "github.com/prometheus/snmp_exporter/collector" 40 | "github.com/prometheus/snmp_exporter/config" 41 | ) 42 | 43 | const ( 44 | namespace = "snmp" 45 | ) 46 | 47 | var ( 48 | configFile = kingpin.Flag("config.file", "Path to configuration file.").Default("snmp.yml").Strings() 49 | dryRun = kingpin.Flag("dry-run", "Only verify configuration is valid and exit.").Default("false").Bool() 50 | concurrency = kingpin.Flag("snmp.module-concurrency", "The number of modules to fetch concurrently per scrape").Default("1").Int() 51 | debugSNMP = kingpin.Flag("snmp.debug-packets", "Include a full debug trace of SNMP packet traffics.").Default("false").Bool() 52 | expandEnvVars = kingpin.Flag("config.expand-environment-variables", "Expand environment variables to source secrets").Default("false").Bool() 53 | metricsPath = kingpin.Flag( 54 | "web.telemetry-path", 55 | "Path under which to expose metrics.", 56 | ).Default("/metrics").String() 57 | toolkitFlags = webflag.AddFlags(kingpin.CommandLine, ":9116") 58 | 59 | // Metrics about the SNMP exporter itself. 60 | snmpRequestErrors = promauto.NewCounter( 61 | prometheus.CounterOpts{ 62 | Namespace: namespace, 63 | Name: "request_errors_total", 64 | Help: "Errors in requests to the SNMP exporter", 65 | }, 66 | ) 67 | snmpCollectionDuration = promauto.NewHistogramVec( 68 | prometheus.HistogramOpts{ 69 | Namespace: namespace, 70 | Name: "collection_duration_seconds", 71 | Help: "Duration of collections by the SNMP exporter", 72 | }, 73 | []string{"module"}, 74 | ) 75 | sc = &SafeConfig{ 76 | C: &config.Config{}, 77 | } 78 | reloadCh chan chan error 79 | ) 80 | 81 | const ( 82 | proberPath = "/snmp" 83 | configPath = "/config" 84 | ) 85 | 86 | func handler(w http.ResponseWriter, r *http.Request, logger *slog.Logger, exporterMetrics collector.Metrics) { 87 | query := r.URL.Query() 88 | 89 | debug := *debugSNMP 90 | if query.Get("snmp_debug_packets") == "true" { 91 | debug = true 92 | // TODO: This doesn't work the way I want. 93 | // logger = level.NewFilter(logger, level.AllowDebug()) 94 | logger.Debug("Debug query param enabled") 95 | } 96 | 97 | target := query.Get("target") 98 | if len(query["target"]) != 1 || target == "" { 99 | http.Error(w, "'target' parameter must be specified once", http.StatusBadRequest) 100 | snmpRequestErrors.Inc() 101 | return 102 | } 103 | 104 | authName := query.Get("auth") 105 | if len(query["auth"]) > 1 { 106 | http.Error(w, "'auth' parameter must only be specified once", http.StatusBadRequest) 107 | snmpRequestErrors.Inc() 108 | return 109 | } 110 | if authName == "" { 111 | authName = "public_v2" 112 | } 113 | 114 | snmpContext := query.Get("snmp_context") 115 | if len(query["snmp_context"]) > 1 { 116 | http.Error(w, "'snmp_context' parameter must only be specified once", http.StatusBadRequest) 117 | snmpRequestErrors.Inc() 118 | return 119 | } 120 | 121 | queryModule := query["module"] 122 | if len(queryModule) == 0 { 123 | queryModule = append(queryModule, "if_mib") 124 | } 125 | uniqueM := make(map[string]bool) 126 | var modules []string 127 | for _, qm := range queryModule { 128 | for _, m := range strings.Split(qm, ",") { 129 | if m == "" { 130 | continue 131 | } 132 | if _, ok := uniqueM[m]; !ok { 133 | uniqueM[m] = true 134 | modules = append(modules, m) 135 | } 136 | } 137 | } 138 | sc.RLock() 139 | auth, authOk := sc.C.Auths[authName] 140 | if !authOk { 141 | sc.RUnlock() 142 | http.Error(w, fmt.Sprintf("Unknown auth '%s'", authName), http.StatusBadRequest) 143 | snmpRequestErrors.Inc() 144 | return 145 | } 146 | var nmodules []*collector.NamedModule 147 | for _, m := range modules { 148 | module, moduleOk := sc.C.Modules[m] 149 | if !moduleOk { 150 | sc.RUnlock() 151 | http.Error(w, fmt.Sprintf("Unknown module '%s'", m), http.StatusBadRequest) 152 | snmpRequestErrors.Inc() 153 | return 154 | } 155 | nmodules = append(nmodules, collector.NewNamedModule(m, module)) 156 | } 157 | sc.RUnlock() 158 | logger = logger.With("auth", authName, "target", target) 159 | registry := prometheus.NewRegistry() 160 | c := collector.New(r.Context(), target, authName, snmpContext, auth, nmodules, logger, exporterMetrics, *concurrency, debug) 161 | registry.MustRegister(c) 162 | // Delegate http serving to Prometheus client library, which will call collector.Collect. 163 | h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 164 | h.ServeHTTP(w, r) 165 | } 166 | 167 | func updateConfiguration(w http.ResponseWriter, r *http.Request) { 168 | switch r.Method { 169 | case "POST": 170 | rc := make(chan error) 171 | reloadCh <- rc 172 | if err := <-rc; err != nil { 173 | http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError) 174 | } 175 | default: 176 | http.Error(w, "POST method expected", http.StatusBadRequest) 177 | } 178 | } 179 | 180 | type SafeConfig struct { 181 | sync.RWMutex 182 | C *config.Config 183 | } 184 | 185 | func (sc *SafeConfig) ReloadConfig(configFile []string, expandEnvVars bool) (err error) { 186 | conf, err := config.LoadFile(configFile, expandEnvVars) 187 | if err != nil { 188 | return err 189 | } 190 | sc.Lock() 191 | sc.C = conf 192 | // Initialize metrics. 193 | for module := range sc.C.Modules { 194 | snmpCollectionDuration.WithLabelValues(module) 195 | } 196 | sc.Unlock() 197 | return nil 198 | } 199 | 200 | func main() { 201 | promslogConfig := &promslog.Config{} 202 | flag.AddFlags(kingpin.CommandLine, promslogConfig) 203 | kingpin.Version(version.Print("snmp_exporter")) 204 | kingpin.HelpFlag.Short('h') 205 | kingpin.Parse() 206 | logger := promslog.New(promslogConfig) 207 | if *concurrency < 1 { 208 | *concurrency = 1 209 | } 210 | 211 | logger.Info("Starting snmp_exporter", "version", version.Info(), "concurrency", concurrency, "debug_snmp", debugSNMP) 212 | logger.Info("operational information", "build_context", version.BuildContext()) 213 | 214 | prometheus.MustRegister(versioncollector.NewCollector("snmp_exporter")) 215 | 216 | // Bail early if the config is bad. 217 | err := sc.ReloadConfig(*configFile, *expandEnvVars) 218 | if err != nil { 219 | logger.Error("Error parsing config file", "err", err) 220 | logger.Error("Possible version missmatch between generator and snmp_exporter. Make sure generator and snmp_exporter are the same version.") 221 | logger.Error("See also: https://github.com/prometheus/snmp_exporter/blob/main/auth-split-migration.md") 222 | os.Exit(1) 223 | } 224 | 225 | // Exit if in dry-run mode. 226 | if *dryRun { 227 | logger.Info("Configuration parsed successfully") 228 | return 229 | } 230 | 231 | hup := make(chan os.Signal, 1) 232 | reloadCh = make(chan chan error) 233 | signal.Notify(hup, syscall.SIGHUP) 234 | go func() { 235 | for { 236 | select { 237 | case <-hup: 238 | if err := sc.ReloadConfig(*configFile, *expandEnvVars); err != nil { 239 | logger.Error("Error reloading config", "err", err) 240 | } else { 241 | logger.Info("Loaded config file") 242 | } 243 | case rc := <-reloadCh: 244 | if err := sc.ReloadConfig(*configFile, *expandEnvVars); err != nil { 245 | logger.Error("Error reloading config", "err", err) 246 | rc <- err 247 | } else { 248 | logger.Info("Loaded config file") 249 | rc <- nil 250 | } 251 | } 252 | } 253 | }() 254 | 255 | buckets := prometheus.ExponentialBuckets(0.0001, 2, 15) 256 | exporterMetrics := collector.Metrics{ 257 | SNMPCollectionDuration: snmpCollectionDuration, 258 | SNMPUnexpectedPduType: promauto.NewCounter( 259 | prometheus.CounterOpts{ 260 | Namespace: namespace, 261 | Name: "unexpected_pdu_type_total", 262 | Help: "Unexpected Go types in a PDU.", 263 | }, 264 | ), 265 | SNMPDuration: promauto.NewHistogram( 266 | prometheus.HistogramOpts{ 267 | Namespace: namespace, 268 | Name: "packet_duration_seconds", 269 | Help: "A histogram of latencies for SNMP packets.", 270 | Buckets: buckets, 271 | }, 272 | ), 273 | SNMPPackets: promauto.NewCounter( 274 | prometheus.CounterOpts{ 275 | Namespace: namespace, 276 | Name: "packets_total", 277 | Help: "Number of SNMP packet sent, including retries.", 278 | }, 279 | ), 280 | SNMPRetries: promauto.NewCounter( 281 | prometheus.CounterOpts{ 282 | Namespace: namespace, 283 | Name: "packet_retries_total", 284 | Help: "Number of SNMP packet retries.", 285 | }, 286 | ), 287 | SNMPInflight: promauto.NewGauge( 288 | prometheus.GaugeOpts{ 289 | Namespace: namespace, 290 | Name: "request_in_flight", 291 | Help: "Current number of SNMP scrapes being requested.", 292 | }, 293 | ), 294 | } 295 | 296 | http.Handle(*metricsPath, promhttp.Handler()) // Normal metrics endpoint for SNMP exporter itself. 297 | // Endpoint to do SNMP scrapes. 298 | http.HandleFunc(proberPath, func(w http.ResponseWriter, r *http.Request) { 299 | handler(w, r, logger, exporterMetrics) 300 | }) 301 | http.HandleFunc("/-/reload", updateConfiguration) // Endpoint to reload configuration. 302 | // Endpoint to respond to health checks 303 | http.HandleFunc("/-/healthy", func(w http.ResponseWriter, r *http.Request) { 304 | w.WriteHeader(http.StatusOK) 305 | w.Write([]byte("Healthy")) 306 | }) 307 | 308 | if *metricsPath != "/" && *metricsPath != "" { 309 | landingConfig := web.LandingConfig{ 310 | Name: "SNMP Exporter", 311 | Description: "Prometheus Exporter for SNMP targets", 312 | Version: version.Info(), 313 | Form: web.LandingForm{ 314 | Action: proberPath, 315 | Inputs: []web.LandingFormInput{ 316 | { 317 | Label: "Target", 318 | Type: "text", 319 | Name: "target", 320 | Placeholder: "X.X.X.X/[::X]", 321 | Value: "::1", 322 | }, 323 | { 324 | Label: "Auth", 325 | Type: "text", 326 | Name: "auth", 327 | Placeholder: "auth", 328 | Value: "public_v2", 329 | }, 330 | { 331 | Label: "Module", 332 | Type: "text", 333 | Name: "module", 334 | Placeholder: "module", 335 | Value: "if_mib", 336 | }, 337 | }, 338 | }, 339 | Links: []web.LandingLinks{ 340 | { 341 | Address: configPath, 342 | Text: "Config", 343 | }, 344 | { 345 | Address: *metricsPath, 346 | Text: "Metrics", 347 | }, 348 | }, 349 | } 350 | landingPage, err := web.NewLandingPage(landingConfig) 351 | if err != nil { 352 | logger.Error("Error creating landing page", "err", err) 353 | os.Exit(1) 354 | } 355 | http.Handle("/", landingPage) 356 | } 357 | 358 | http.HandleFunc(configPath, func(w http.ResponseWriter, r *http.Request) { 359 | sc.RLock() 360 | c, err := yaml.Marshal(sc.C) 361 | sc.RUnlock() 362 | if err != nil { 363 | logger.Error("Error marshaling configuration", "err", err) 364 | http.Error(w, err.Error(), http.StatusInternalServerError) 365 | return 366 | } 367 | w.Write(c) 368 | }) 369 | 370 | srv := &http.Server{} 371 | if err := web.ListenAndServe(srv, toolkitFlags, logger); err != nil { 372 | logger.Error("Error starting HTTP server", "err", err) 373 | os.Exit(1) 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /scraper/gosnmp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 scraper 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "log/slog" 20 | "net" 21 | "strconv" 22 | "strings" 23 | "time" 24 | 25 | "github.com/gosnmp/gosnmp" 26 | ) 27 | 28 | type GoSNMPWrapper struct { 29 | c *gosnmp.GoSNMP 30 | logger *slog.Logger 31 | } 32 | 33 | func NewGoSNMP(logger *slog.Logger, target, srcAddress string, debug bool) (*GoSNMPWrapper, error) { 34 | transport := "udp" 35 | if s := strings.SplitN(target, "://", 2); len(s) == 2 { 36 | transport = s[0] 37 | target = s[1] 38 | } 39 | port := uint16(161) 40 | if host, _port, err := net.SplitHostPort(target); err == nil { 41 | target = host 42 | p, err := strconv.Atoi(_port) 43 | if err != nil { 44 | return nil, fmt.Errorf("error converting port number to int for target %q: %w", target, err) 45 | } 46 | port = uint16(p) 47 | } 48 | g := &gosnmp.GoSNMP{ 49 | Transport: transport, 50 | Target: target, 51 | Port: port, 52 | LocalAddr: srcAddress, 53 | } 54 | if debug { 55 | g.Logger = gosnmp.NewLogger(slog.NewLogLogger(logger.Handler(), slog.LevelDebug)) 56 | } 57 | return &GoSNMPWrapper{c: g, logger: logger}, nil 58 | } 59 | 60 | func (g *GoSNMPWrapper) SetOptions(fns ...func(*gosnmp.GoSNMP)) { 61 | for _, fn := range fns { 62 | fn(g.c) 63 | } 64 | } 65 | 66 | func (g *GoSNMPWrapper) Connect() error { 67 | st := time.Now() 68 | err := g.c.Connect() 69 | if err != nil { 70 | if err == context.Canceled { 71 | return fmt.Errorf("scrape cancelled after %s (possible timeout) connecting to target %s", 72 | time.Since(st), g.c.Target) 73 | } 74 | return fmt.Errorf("error connecting to target %s: %s", g.c.Target, err) 75 | } 76 | return nil 77 | } 78 | 79 | func (g *GoSNMPWrapper) Close() error { 80 | return g.c.Conn.Close() 81 | } 82 | 83 | func (g *GoSNMPWrapper) Get(oids []string) (results *gosnmp.SnmpPacket, err error) { 84 | g.logger.Debug("Getting OIDs", "oids", oids) 85 | st := time.Now() 86 | results, err = g.c.Get(oids) 87 | if err != nil { 88 | if err == context.Canceled { 89 | err = fmt.Errorf("scrape cancelled after %s (possible timeout) getting target %s", 90 | time.Since(st), g.c.Target) 91 | } else { 92 | err = fmt.Errorf("error getting target %s: %s", g.c.Target, err) 93 | } 94 | return 95 | } 96 | g.logger.Debug("Get of OIDs completed", "oids", oids, "duration_seconds", time.Since(st)) 97 | return 98 | } 99 | 100 | func (g *GoSNMPWrapper) WalkAll(oid string) (results []gosnmp.SnmpPDU, err error) { 101 | g.logger.Debug("Walking subtree", "oid", oid) 102 | st := time.Now() 103 | if g.c.Version == gosnmp.Version1 { 104 | results, err = g.c.WalkAll(oid) 105 | } else { 106 | results, err = g.c.BulkWalkAll(oid) 107 | } 108 | if err != nil { 109 | if err == context.Canceled { 110 | err = fmt.Errorf("scrape canceled after %s (possible timeout) walking target %s", 111 | time.Since(st), g.c.Target) 112 | } else { 113 | err = fmt.Errorf("error walking target %s: %s", g.c.Target, err) 114 | } 115 | return 116 | } 117 | g.logger.Debug("Walk of subtree completed", "oid", oid, "duration_seconds", time.Since(st)) 118 | return 119 | } 120 | -------------------------------------------------------------------------------- /scraper/mock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 scraper 15 | 16 | import ( 17 | "github.com/gosnmp/gosnmp" 18 | ) 19 | 20 | func NewMockSNMPScraper(get map[string]gosnmp.SnmpPDU, walk map[string][]gosnmp.SnmpPDU) *mockSNMPScraper { 21 | return &mockSNMPScraper{ 22 | GetResponses: get, 23 | WalkResponses: walk, 24 | callGet: make([]string, 0), 25 | callWalk: make([]string, 0), 26 | } 27 | } 28 | 29 | type mockSNMPScraper struct { 30 | GetResponses map[string]gosnmp.SnmpPDU 31 | WalkResponses map[string][]gosnmp.SnmpPDU 32 | ConnectError error 33 | CloseError error 34 | 35 | callGet []string 36 | callWalk []string 37 | } 38 | 39 | func (m *mockSNMPScraper) CallGet() []string { 40 | return m.callGet 41 | } 42 | 43 | func (m *mockSNMPScraper) CallWalk() []string { 44 | return m.callWalk 45 | } 46 | 47 | func (m *mockSNMPScraper) Get(oids []string) (*gosnmp.SnmpPacket, error) { 48 | pdus := make([]gosnmp.SnmpPDU, 0, len(oids)) 49 | for _, oid := range oids { 50 | if response, exists := m.GetResponses[oid]; exists { 51 | pdus = append(pdus, response) 52 | } else { 53 | pdus = append(pdus, gosnmp.SnmpPDU{ 54 | Name: oid, 55 | Type: gosnmp.NoSuchObject, 56 | Value: nil, 57 | }) 58 | } 59 | m.callGet = append(m.callGet, oid) 60 | } 61 | return &gosnmp.SnmpPacket{ 62 | Variables: pdus, 63 | Error: gosnmp.NoError, 64 | }, nil 65 | } 66 | 67 | func (m *mockSNMPScraper) WalkAll(baseOID string) ([]gosnmp.SnmpPDU, error) { 68 | m.callWalk = append(m.callWalk, baseOID) 69 | if pdus, exists := m.WalkResponses[baseOID]; exists { 70 | return pdus, nil 71 | } 72 | return nil, nil 73 | } 74 | 75 | func (m *mockSNMPScraper) Connect() error { 76 | return m.ConnectError 77 | } 78 | 79 | func (m *mockSNMPScraper) Close() error { 80 | return m.CloseError 81 | } 82 | 83 | func (m *mockSNMPScraper) SetOptions(...func(*gosnmp.GoSNMP)) { 84 | } 85 | -------------------------------------------------------------------------------- /scraper/scraper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 scraper 15 | 16 | import ( 17 | "github.com/gosnmp/gosnmp" 18 | ) 19 | 20 | type SNMPScraper interface { 21 | Get([]string) (*gosnmp.SnmpPacket, error) 22 | WalkAll(string) ([]gosnmp.SnmpPDU, error) 23 | Connect() error 24 | Close() error 25 | SetOptions(...func(*gosnmp.GoSNMP)) 26 | } 27 | -------------------------------------------------------------------------------- /snmp-mixin/Makefile: -------------------------------------------------------------------------------- 1 | JSONNET_FMT := jsonnetfmt -n 2 --max-blank-lines 2 --string-style s --comment-style s 2 | 3 | all: fmt lint build clean 4 | 5 | fmt: 6 | find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \ 7 | xargs -n 1 -- $(JSONNET_FMT) -i 8 | 9 | lint: 10 | find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \ 11 | while read f; do \ 12 | $(JSONNET_FMT) "$$f" | diff -u "$$f" -; \ 13 | done 14 | 15 | mixtool lint mixin.libsonnet 16 | 17 | build: 18 | mixtool generate all mixin.libsonnet 19 | 20 | clean: 21 | rm -rf dashboards_out alerts.yaml rules.yaml 22 | -------------------------------------------------------------------------------- /snmp-mixin/README.md: -------------------------------------------------------------------------------- 1 | # SNMP Mixin 2 | 3 | The SNMP Mixin is a set of configurable, reusable, and extensible alerts and 4 | dashboards based on the metrics exported by snmp_exporter. The mixin creates 5 | recording and alerting rules for Prometheus and suitable dashboard descriptions 6 | for Grafana. 7 | 8 | To use them, you need to have `mixtool` and `jsonnetfmt` installed. If you 9 | have a working Go development environment, it's easiest to run the following: 10 | ```bash 11 | $ go install github.com/monitoring-mixins/mixtool/cmd/mixtool@latest 12 | $ go install github.com/google/go-jsonnet/cmd/jsonnetfmt@latest 13 | ``` 14 | 15 | You can then build the Prometheus rules files `alerts.yaml` and 16 | `rules.yaml` and a directory `dashboard_out` with the JSON dashboard files 17 | for Grafana: 18 | ```sh 19 | $ make build 20 | ``` 21 | 22 | For more advanced uses of mixins, see 23 | https://github.com/monitoring-mixins/docs. 24 | -------------------------------------------------------------------------------- /snmp-mixin/alerts/snmp_general.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: SNMPAlerts 3 | rules: 4 | - alert: SNMPDown 5 | expr: up{job=~"snmp.*"} != 1 6 | for: 5m 7 | labels: 8 | severity: critical 9 | annotations: 10 | description: 'SNMP device {{$labels.instance}} from job {{$labels.job}} is down.' 11 | summary: SNMP device down. 12 | -------------------------------------------------------------------------------- /snmp-mixin/alerts/snmp_ubiquiti_wifi.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: SNMPUbiquitiAlerts 3 | rules: 4 | - alert: SNMPUbiquitiRxErrorRate 5 | expr: (sum by (instance) (irate(unifiVapRxDropped{job=~"snmp.*"}[5m])) + sum by (instance) (irate(unifiVapRxErrors{job=~"snmp.*"}[5m]))) / sum by (instance) (irate(unifiVapRxPackets{job=~"snmp.*"}[5m])) > 0.2 6 | for: 5m 7 | labels: 8 | severity: warning 9 | annotations: 10 | description: 'Access point {{$labels.instance}} from job {{$labels.job}} has input error rate > 20%.' 11 | summary: High input error rate. 12 | - alert: SNMPUbiquitiTxErrorRate 13 | expr: (sum by (instance) (irate(unifiVapTxDropped{job=~"snmp.*"}[5m])) + sum by (instance) (irate(unifiVapTxErrors{job=~"snmp.*"}[5m]))) / sum by (instance) (irate(unifiVapTxPackets{job=~"snmp.*"}[5m])) > 0.05 14 | for: 5m 15 | labels: 16 | severity: warning 17 | annotations: 18 | description: 'Access point {{$labels.instance}} from job {{$labels.job}} has output error rate > 5%.' 19 | summary: High output error rate. 20 | -------------------------------------------------------------------------------- /snmp-mixin/mixin.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | grafanaDashboards: { 3 | 'snmp-ubiquiti-wifi.json': (import 'dashboards/snmp_ubiquiti_wifi.json'), 4 | 'snmp-ubiquiti-access_point.json': (import 'dashboards/snmp_ubiquiti_access_point.json'), 5 | }, 6 | 7 | // Helper function to ensure that we don't override other rules, by forcing 8 | // the patching of the groups list, and not the overall rules object. 9 | local importRules(rules) = { 10 | groups+: std.native('parseYaml')(rules)[0].groups, 11 | }, 12 | 13 | prometheusRules+: importRules(importstr 'rules/rules.yaml'), 14 | 15 | prometheusAlerts+: 16 | importRules(importstr 'alerts/snmp_general.yml') + 17 | importRules(importstr 'alerts/snmp_ubiquiti_wifi.yml'), 18 | } 19 | -------------------------------------------------------------------------------- /snmp-mixin/rules/rules.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | -------------------------------------------------------------------------------- /testdata/snmp-auth-envvars.yml: -------------------------------------------------------------------------------- 1 | auths: 2 | with_secret: 3 | community: mysecret 4 | security_level: SomethingReadOnly 5 | username: ${ENV_USERNAME} 6 | password: ${ENV_PASSWORD} 7 | auth_protocol: SHA256 8 | priv_protocol: AES 9 | priv_password: ${ENV_PRIV_PASSWORD} 10 | -------------------------------------------------------------------------------- /testdata/snmp-auth-v2nocreds.yml: -------------------------------------------------------------------------------- 1 | auths: 2 | v2_without_secret: 3 | community: mysecret 4 | version: 2 5 | -------------------------------------------------------------------------------- /testdata/snmp-auth.yml: -------------------------------------------------------------------------------- 1 | auths: 2 | with_secret: 3 | community: mysecret 4 | security_level: SomethingReadOnly 5 | username: user 6 | password: mysecret 7 | auth_protocol: SHA256 8 | priv_protocol: AES 9 | priv_password: mysecret 10 | -------------------------------------------------------------------------------- /testdata/snmp-with-overrides.yml: -------------------------------------------------------------------------------- 1 | modules: 2 | default: 3 | walk: 4 | - 1.1.1.1.1.1 5 | metrics: 6 | - name: testMetric 7 | oid: 1.1.1.1.1 8 | type: gauge 9 | regex_extracts: 10 | Temp: 11 | - regex: 12 | --------------------------------------------------------------------------------