diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 12:08:03 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 12:08:18 +0000 |
commit | 5da14042f70711ea5cf66e034699730335462f66 (patch) | |
tree | 0f6354ccac934ed87a2d555f45be4c831cf92f4a /src/go/collectors/go.d.plugin/modules/snmp | |
parent | Releasing debian version 1.44.3-2. (diff) | |
download | netdata-5da14042f70711ea5cf66e034699730335462f66.tar.xz netdata-5da14042f70711ea5cf66e034699730335462f66.zip |
Merging upstream version 1.45.3+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
11 files changed, 2335 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/snmp/README.md b/src/go/collectors/go.d.plugin/modules/snmp/README.md new file mode 120000 index 000000000..edf223bf9 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/README.md @@ -0,0 +1 @@ +integrations/snmp_devices.md
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/snmp/charts.go b/src/go/collectors/go.d.plugin/modules/snmp/charts.go new file mode 100644 index 000000000..9899ec7aa --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/charts.go @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package snmp + +import ( + "fmt" + "strings" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" +) + +func newCharts(configs []ChartConfig) (*module.Charts, error) { + charts := &module.Charts{} + for _, cfg := range configs { + if len(cfg.IndexRange) == 2 { + cs, err := newChartsFromIndexRange(cfg) + if err != nil { + return nil, err + } + if err := charts.Add(*cs...); err != nil { + return nil, err + } + } else { + chart, err := newChart(cfg) + if err != nil { + return nil, err + } + if err = charts.Add(chart); err != nil { + return nil, err + } + } + } + return charts, nil +} + +func newChartsFromIndexRange(cfg ChartConfig) (*module.Charts, error) { + var addPrio int + charts := &module.Charts{} + for i := cfg.IndexRange[0]; i <= cfg.IndexRange[1]; i++ { + chart, err := newChartWithOIDIndex(i, cfg) + if err != nil { + return nil, err + } + chart.Priority += addPrio + addPrio += 1 + if err = charts.Add(chart); err != nil { + return nil, err + } + } + return charts, nil +} + +func newChartWithOIDIndex(oidIndex int, cfg ChartConfig) (*module.Chart, error) { + chart, err := newChart(cfg) + if err != nil { + return nil, err + } + + chart.ID = fmt.Sprintf("%s_%d", chart.ID, oidIndex) + chart.Title = fmt.Sprintf("%s %d", chart.Title, oidIndex) + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf("%s.%d", dim.ID, oidIndex) + } + + return chart, nil +} + +func newChart(cfg ChartConfig) (*module.Chart, error) { + chart := &module.Chart{ + ID: cfg.ID, + Title: cfg.Title, + Units: cfg.Units, + Fam: cfg.Family, + Ctx: fmt.Sprintf("snmp.%s", cfg.ID), + Type: module.ChartType(cfg.Type), + Priority: cfg.Priority, + } + + if chart.Title == "" { + chart.Title = "Untitled chart" + } + if chart.Units == "" { + chart.Units = "num" + } + if chart.Priority < module.Priority { + chart.Priority += module.Priority + } + + seen := make(map[string]struct{}) + var a string + for _, cfg := range cfg.Dimensions { + if cfg.Algorithm != "" { + seen[cfg.Algorithm] = struct{}{} + a = cfg.Algorithm + } + dim := &module.Dim{ + ID: strings.TrimPrefix(cfg.OID, "."), + Name: cfg.Name, + Algo: module.DimAlgo(cfg.Algorithm), + Mul: cfg.Multiplier, + Div: cfg.Divisor, + } + if err := chart.AddDim(dim); err != nil { + return nil, err + } + } + if len(seen) == 1 && a != "" && len(chart.Dims) > 1 { + for _, d := range chart.Dims { + if d.Algo == "" { + d.Algo = module.DimAlgo(a) + } + } + } + + return chart, nil +} diff --git a/src/go/collectors/go.d.plugin/modules/snmp/collect.go b/src/go/collectors/go.d.plugin/modules/snmp/collect.go new file mode 100644 index 000000000..9f0e78d7e --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/collect.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package snmp + +import ( + "github.com/gosnmp/gosnmp" +) + +func (s *SNMP) collect() (map[string]int64, error) { + collected := make(map[string]int64) + + if err := s.collectOIDs(collected); err != nil { + return nil, err + } + + return collected, nil +} + +func (s *SNMP) collectOIDs(collected map[string]int64) error { + for i, end := 0, 0; i < len(s.oids); i += s.Options.MaxOIDs { + if end = i + s.Options.MaxOIDs; end > len(s.oids) { + end = len(s.oids) + } + + oids := s.oids[i:end] + resp, err := s.snmpClient.Get(oids) + if err != nil { + s.Errorf("cannot get SNMP data: %v", err) + return err + } + + for i, oid := range oids { + if i >= len(resp.Variables) { + continue + } + + switch v := resp.Variables[i]; v.Type { + case gosnmp.Boolean, + gosnmp.Counter32, + gosnmp.Counter64, + gosnmp.Gauge32, + gosnmp.TimeTicks, + gosnmp.Uinteger32, + gosnmp.OpaqueFloat, + gosnmp.OpaqueDouble, + gosnmp.Integer: + collected[oid] = gosnmp.ToBigInt(v.Value).Int64() + default: + s.Debugf("skipping OID '%s' (unsupported type '%s')", oid, v.Type) + } + } + } + + return nil +} diff --git a/src/go/collectors/go.d.plugin/modules/snmp/config_schema.json b/src/go/collectors/go.d.plugin/modules/snmp/config_schema.json new file mode 100644 index 000000000..a83a2da36 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/config_schema.json @@ -0,0 +1,379 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "hostname": { + "title": "Hostname", + "description": "The hostname or IP address of the SNMP-enabled device.", + "type": "string" + }, + "community": { + "title": "SNMPv1/2 community", + "description": "The SNMP community string for SNMPv1/v2c authentication.", + "type": "string", + "default": "public" + }, + "options": { + "title": "Options", + "description": "Configuration options for SNMP monitoring.", + "type": [ + "object", + "null" + ], + "properties": { + "version": { + "title": "SNMP version", + "type": "string", + "enum": [ + "1", + "2c", + "3" + ], + "default": "2c" + }, + "port": { + "title": "Port", + "description": "The port number on which the SNMP service is running.", + "type": "integer", + "exclusiveMinimum": 0, + "default": 161 + }, + "retries": { + "title": "Retries", + "description": "The number of retries to attempt for SNMP requests.", + "type": "integer", + "minimum": 0, + "default": 161 + }, + "timeout": { + "title": "Timeout", + "description": "The timeout duration in seconds for SNMP requests.", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "max_request_size": { + "title": "Max OIDs in request", + "description": "The maximum number of OIDs allowed in a single SNMP request.", + "type": "integer", + "minimum": 1, + "default": 60 + } + }, + "required": [ + "version", + "port", + "retries", + "timeout", + "max_request_size" + ] + }, + "user": { + "title": "SNMPv3 configuration", + "description": "Configuration options for SNMPv3 authentication and encryption.", + "type": [ + "object", + "null" + ], + "properties": { + "name": { + "title": "Username", + "description": "The username for SNMPv3 authentication.", + "type": "string" + }, + "level": { + "title": "Security level", + "description": "Controls the security aspects of SNMPv3 communication, including authentication and encryption.", + "type": "string", + "enum": [ + "none", + "authNoPriv", + "authPriv" + ], + "default": "authPriv" + }, + "auth_proto": { + "title": "Authentication protocol", + "type": "string", + "enum": [ + "none", + "md5", + "sha", + "sha224", + "sha256", + "sha384", + "sha512" + ], + "default": "sha512" + }, + "auth_key": { + "title": "Authentication passphrase", + "type": "string" + }, + "priv_proto": { + "title": "Privacy protocol", + "type": "string", + "enum": [ + "none", + "des", + "aes", + "aes192", + "aes256", + "aes192c" + ], + "default": "aes192c" + }, + "priv_key": { + "title": "Privacy passphrase", + "type": "string" + } + } + }, + "charts": { + "title": "Charts configuration", + "type": [ + "array", + "null" + ], + "uniqueItems": true, + "minItems": 1, + "items": { + "title": "Chart", + "type": [ + "object", + "null" + ], + "properties": { + "id": { + "title": "ID", + "description": "Unique identifier for the chart.", + "type": "string" + }, + "title": { + "title": "Title", + "description": "Title of the chart.", + "type": "string" + }, + "units": { + "title": "Units", + "description": "Unit label for the vertical axis on charts.", + "type": "string" + }, + "family": { + "title": "Family", + "description": "Subsection on the dashboard where the chart will be displayed.", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string", + "enum": [ + "line", + "area", + "stacked" + ], + "default": "line" + }, + "priority": { + "title": "Priority", + "description": "Rendering priority of the chart on the dashboard. Lower priority values will cause the chart to appear before those with higher priority values.", + "type": "integer", + "minimum": 1, + "default": 90000 + }, + "multiply_range": { + "title": "OID index range", + "description": "Specifies the range of indexes used to create multiple charts. If set, a chart will be created for each index in the specified range. Each chart will have the index appended to the OID dimension.", + "type": [ + "array", + "null" + ], + "items": { + "title": "Index", + "type": "integer", + "minimum": 0 + }, + "uniqueItems": true, + "maxItems": 2 + }, + "dimensions": { + "title": "Dimensions", + "description": "Configuration for dimensions of the chart.", + "type": [ + "array", + "null" + ], + "uniqueItems": true, + "minItems": 1, + "items": { + "title": "Dimension configuration", + "type": [ + "object", + "null" + ], + "properties": { + "oid": { + "title": "OID", + "description": "SNMP OID.", + "type": "string" + }, + "name": { + "title": "Dimension", + "description": "Name of the dimension.", + "type": "string" + }, + "algorithm": { + "title": "Algorithm", + "description": "Algorithm of the dimension.", + "type": "string", + "enum": [ + "absolute", + "incremental" + ], + "default": "absolute" + }, + "multiplier": { + "title": "Multiplier", + "description": "Value to multiply the collected value.", + "type": "integer", + "not": { + "const": 0 + }, + "default": 1 + }, + "divisor": { + "title": "Divisor", + "description": "Value to divide the collected value.", + "type": "integer", + "not": { + "const": 0 + }, + "default": 1 + } + }, + "required": [ + "oid", + "name", + "algorithm", + "multiplier", + "divisor" + ] + } + } + }, + "required": [ + "id", + "title", + "units", + "family", + "type", + "priority", + "dimensions" + ] + } + } + }, + "required": [ + "hostname", + "community", + "options", + "charts" + ], + "additionalProperties": false, + "patternProperties": { + "^name$": {} + } + }, + "uiSchema": { + "uiOptions": { + "fullPage": true + }, + "options": { + "version": { + "ui:widget": "radio", + "ui:options": { + "inline": true + } + } + }, + "user": { + "level": { + "ui:widget": "radio", + "ui:options": { + "inline": true + } + }, + "auth_proto": { + "ui:widget": "radio", + "ui:options": { + "inline": true + } + }, + "priv_proto": { + "ui:widget": "radio", + "ui:options": { + "inline": true + } + } + }, + "charts": { + "items": { + "ui:collapsible": true, + "type": { + "ui:widget": "radio", + "ui:options": { + "inline": true + } + }, + "multiply_range": { + "ui:listFlavour": "list" + }, + "dimensions": { + "items": { + "ui:collapsible": true, + "algorithm": { + "ui:widget": "radio", + "ui:options": { + "inline": true + } + } + } + } + } + }, + "ui:flavour": "tabs", + "ui:options": { + "tabs": [ + { + "title": "Base", + "fields": [ + "update_every", + "hostname", + "community", + "options" + ] + }, + { + "title": "SNMPv3", + "fields": [ + "user" + ] + }, + { + "title": "Charts", + "fields": [ + "charts" + ] + } + ] + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/snmp/init.go b/src/go/collectors/go.d.plugin/modules/snmp/init.go new file mode 100644 index 000000000..5802d6682 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/init.go @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package snmp + +import ( + "errors" + "fmt" + "time" + + "github.com/gosnmp/gosnmp" +) + +var newSNMPClient = gosnmp.NewHandler + +func (s *SNMP) validateConfig() error { + if len(s.ChartsInput) == 0 { + return errors.New("'charts' are required but not set") + } + + if s.Options.Version == gosnmp.Version3.String() { + if s.User.Name == "" { + return errors.New("'user.name' is required when using SNMPv3 but not set") + } + if _, err := parseSNMPv3SecurityLevel(s.User.SecurityLevel); err != nil { + return err + } + if _, err := parseSNMPv3AuthProtocol(s.User.AuthProto); err != nil { + return err + } + if _, err := parseSNMPv3PrivProtocol(s.User.PrivProto); err != nil { + return err + } + } + + return nil +} + +func (s *SNMP) initSNMPClient() (gosnmp.Handler, error) { + client := newSNMPClient() + + if client.SetTarget(s.Hostname); client.Target() == "" { + s.Warningf("'hostname' not set, using the default value: '%s'", defaultHostname) + client.SetTarget(defaultHostname) + } + if client.SetPort(uint16(s.Options.Port)); client.Port() <= 0 || client.Port() > 65535 { + s.Warningf("'options.port' is invalid, changing to the default value: '%d' => '%d'", s.Options.Port, defaultPort) + client.SetPort(defaultPort) + } + if client.SetRetries(s.Options.Retries); client.Retries() < 1 || client.Retries() > 10 { + s.Warningf("'options.retries' is invalid, changing to the default value: '%d' => '%d'", s.Options.Retries, defaultRetries) + client.SetRetries(defaultRetries) + } + if client.SetTimeout(time.Duration(s.Options.Timeout) * time.Second); client.Timeout().Seconds() < 1 { + s.Warningf("'options.timeout' is invalid, changing to the default value: '%d' => '%d'", s.Options.Timeout, defaultTimeout) + client.SetTimeout(defaultTimeout * time.Second) + } + if client.SetMaxOids(s.Options.MaxOIDs); client.MaxOids() < 1 { + s.Warningf("'options.max_request_size' is invalid, changing to the default value: '%d' => '%d'", s.Options.MaxOIDs, defaultMaxOIDs) + client.SetMaxOids(defaultMaxOIDs) + } + + ver, err := parseSNMPVersion(s.Options.Version) + if err != nil { + s.Warningf("'options.version' is invalid, changing to the default value: '%s' => '%s'", + s.Options.Version, defaultVersion) + ver = defaultVersion + } + comm := s.Community + if comm == "" && (ver <= gosnmp.Version2c) { + s.Warningf("'community' not set, using the default value: '%s'", defaultCommunity) + comm = defaultCommunity + } + + switch ver { + case gosnmp.Version1: + client.SetCommunity(comm) + client.SetVersion(gosnmp.Version1) + case gosnmp.Version2c: + client.SetCommunity(comm) + client.SetVersion(gosnmp.Version2c) + case gosnmp.Version3: + client.SetVersion(gosnmp.Version3) + client.SetSecurityModel(gosnmp.UserSecurityModel) + client.SetMsgFlags(safeParseSNMPv3SecurityLevel(s.User.SecurityLevel)) + client.SetSecurityParameters(&gosnmp.UsmSecurityParameters{ + UserName: s.User.Name, + AuthenticationProtocol: safeParseSNMPv3AuthProtocol(s.User.AuthProto), + AuthenticationPassphrase: s.User.AuthKey, + PrivacyProtocol: safeParseSNMPv3PrivProtocol(s.User.PrivProto), + PrivacyPassphrase: s.User.PrivKey, + }) + default: + return nil, fmt.Errorf("invalid SNMP version: %s", s.Options.Version) + } + + return client, nil +} + +func (s *SNMP) initOIDs() (oids []string) { + for _, c := range *s.charts { + for _, d := range c.Dims { + oids = append(oids, d.ID) + } + } + return oids +} + +func parseSNMPVersion(version string) (gosnmp.SnmpVersion, error) { + switch version { + case "0", "1": + return gosnmp.Version1, nil + case "2", "2c", "": + return gosnmp.Version2c, nil + case "3": + return gosnmp.Version3, nil + default: + return gosnmp.Version2c, fmt.Errorf("invalid snmp version value (%s)", version) + } +} + +func safeParseSNMPv3SecurityLevel(level string) gosnmp.SnmpV3MsgFlags { + v, _ := parseSNMPv3SecurityLevel(level) + return v +} + +func parseSNMPv3SecurityLevel(level string) (gosnmp.SnmpV3MsgFlags, error) { + switch level { + case "1", "none", "noAuthNoPriv", "": + return gosnmp.NoAuthNoPriv, nil + case "2", "authNoPriv": + return gosnmp.AuthNoPriv, nil + case "3", "authPriv": + return gosnmp.AuthPriv, nil + default: + return gosnmp.NoAuthNoPriv, fmt.Errorf("invalid snmpv3 user security level value (%s)", level) + } +} + +func safeParseSNMPv3AuthProtocol(protocol string) gosnmp.SnmpV3AuthProtocol { + v, _ := parseSNMPv3AuthProtocol(protocol) + return v +} + +func parseSNMPv3AuthProtocol(protocol string) (gosnmp.SnmpV3AuthProtocol, error) { + switch protocol { + case "1", "none", "noAuth", "": + return gosnmp.NoAuth, nil + case "2", "md5": + return gosnmp.MD5, nil + case "3", "sha": + return gosnmp.SHA, nil + case "4", "sha224": + return gosnmp.SHA224, nil + case "5", "sha256": + return gosnmp.SHA256, nil + case "6", "sha384": + return gosnmp.SHA384, nil + case "7", "sha512": + return gosnmp.SHA512, nil + default: + return gosnmp.NoAuth, fmt.Errorf("invalid snmpv3 user auth protocol value (%s)", protocol) + } +} + +func safeParseSNMPv3PrivProtocol(protocol string) gosnmp.SnmpV3PrivProtocol { + v, _ := parseSNMPv3PrivProtocol(protocol) + return v +} + +func parseSNMPv3PrivProtocol(protocol string) (gosnmp.SnmpV3PrivProtocol, error) { + switch protocol { + case "1", "none", "noPriv", "": + return gosnmp.NoPriv, nil + case "2", "des": + return gosnmp.DES, nil + case "3", "aes": + return gosnmp.AES, nil + case "4", "aes192": + return gosnmp.AES192, nil + case "5", "aes256": + return gosnmp.AES256, nil + case "6", "aes192c": + return gosnmp.AES192C, nil + case "7", "aes256c": + return gosnmp.AES256C, nil + default: + return gosnmp.NoPriv, fmt.Errorf("invalid snmpv3 user priv protocol value (%s)", protocol) + } +} diff --git a/src/go/collectors/go.d.plugin/modules/snmp/integrations/snmp_devices.md b/src/go/collectors/go.d.plugin/modules/snmp/integrations/snmp_devices.md new file mode 100644 index 000000000..c83e4bcc0 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/integrations/snmp_devices.md @@ -0,0 +1,404 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/snmp/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/snmp/metadata.yaml" +sidebar_label: "SNMP devices" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/Generic Collecting Metrics" +most_popular: True +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# SNMP devices + + +<img src="https://netdata.cloud/img/snmp.png" width="150"/> + + +Plugin: go.d.plugin +Module: snmp + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors any SNMP devices and uses the [gosnmp](https://github.com/gosnmp/gosnmp) package. + +It supports: + +- all SNMP versions: SNMPv1, SNMPv2c and SNMPv3. +- any number of SNMP devices. +- each SNMP device can be used to collect data for any number of charts. +- each chart may have any number of dimensions. +- each SNMP device may have a different update frequency. +- each SNMP device will accept one or more batches to report values (you can set `max_request_size` per SNMP server, to control the size of batches). + +Keep in mind that many SNMP switches and routers are very slow. They may not be able to report values per second. +`go.d.plugin` reports the time it took for the SNMP device to respond when executed in the debug mode. + +Also, if many SNMP clients are used on the same SNMP device at the same time, values may be skipped. +This is a problem of the SNMP device, not this collector. In this case, consider reducing the frequency of data collection (increasing `update_every`). + + + + +This collector is supported on all platforms. + +This collector supports collecting metrics from multiple instances of this integration, including remote instances. + + +### Default Behavior + +#### Auto-Detection + +This integration doesn't support auto-detection. + +#### Limits + +The default configuration for this integration does not impose any limits on data collection. + +#### Performance Impact + +The default configuration for this integration is not expected to impose a significant performance impact on the system. + + +## Metrics + +The metrics that will be collected are defined in the configuration file. + + +## Alerts + +There are no alerts configured by default for this integration. + + +## Setup + +### Prerequisites + +#### Find OIDs + +Use `snmpwalk`, like this: + +```sh +snmpwalk -t 20 -O fn -v 2c -c public 192.0.2.1 +``` + +- `-t 20` is the timeout in seconds. +- `-O fn` will display full OIDs in numeric format. +- `-v 2c` is the SNMP version. +- `-c public` is the SNMP community. +- `192.0.2.1` is the SNMP device. + + + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/snmp.conf`. + + +You can edit the configuration file using the `edit-config` script from the +Netdata [config directory](https://github.com/netdata/netdata/blob/master/docs/netdata-agent/configuration.md#the-netdata-config-directory). + +```bash +cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata +sudo ./edit-config go.d/snmp.conf +``` +#### Options + +The following options can be defined globally: update_every, autodetection_retry. + + +<details><summary>Config options</summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| update_every | Data collection frequency. | 1 | no | +| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no | +| hostname | Target ipv4 address. | 127.0.0.1 | yes | +| community | SNMPv1/2 community string. | public | no | +| options.version | SNMP version. Available versions: 1, 2, 3. | 2 | no | +| options.port | Target port. | 161 | no | +| options.retries | Retries to attempt. | 1 | no | +| options.timeout | SNMP request/response timeout. | 10 | no | +| options.max_request_size | Maximum number of OIDs allowed in one one SNMP request. | 60 | no | +| user.name | SNMPv3 user name. | | no | +| user.name | Security level of SNMPv3 messages. | | no | +| user.auth_proto | Security level of SNMPv3 messages. | | no | +| user.name | Authentication protocol for SNMPv3 messages. | | no | +| user.auth_key | Authentication protocol pass phrase. | | no | +| user.priv_proto | Privacy protocol for SNMPv3 messages. | | no | +| user.priv_key | Privacy protocol pass phrase. | | no | +| charts | List of charts. | [] | yes | +| charts.id | Chart ID. Used to uniquely identify the chart. | | yes | +| charts.title | Chart title. | Untitled chart | no | +| charts.units | Chart units. | num | no | +| charts.family | Chart family. | charts.id | no | +| charts.type | Chart type (line, area, stacked). | line | no | +| charts.priority | Chart priority. | 70000 | no | +| charts.multiply_range | Used when you need to define many charts using incremental OIDs. | [] | no | +| charts.dimensions | List of chart dimensions. | [] | yes | +| charts.dimensions.oid | Collected metric OID. | | yes | +| charts.dimensions.name | Dimension name. | | yes | +| charts.dimensions.algorithm | Dimension algorithm (absolute, incremental). | absolute | no | +| charts.dimensions.multiplier | Collected value multiplier, applied to convert it properly to units. | 1 | no | +| charts.dimensions.divisor | Collected value divisor, applied to convert it properly to units. | 1 | no | + +##### user.auth_proto + +The security of an SNMPv3 message as per RFC 3414 (`user.level`): + +| String value | Int value | Description | +|:------------:|:---------:|------------------------------------------| +| none | 1 | no message authentication or encryption | +| authNoPriv | 2 | message authentication and no encryption | +| authPriv | 3 | message authentication and encryption | + + +##### user.name + +The digest algorithm for SNMPv3 messages that require authentication (`user.auth_proto`): + +| String value | Int value | Description | +|:------------:|:---------:|-------------------------------------------| +| none | 1 | no message authentication | +| md5 | 2 | MD5 message authentication (HMAC-MD5-96) | +| sha | 3 | SHA message authentication (HMAC-SHA-96) | +| sha224 | 4 | SHA message authentication (HMAC-SHA-224) | +| sha256 | 5 | SHA message authentication (HMAC-SHA-256) | +| sha384 | 6 | SHA message authentication (HMAC-SHA-384) | +| sha512 | 7 | SHA message authentication (HMAC-SHA-512) | + + +##### user.priv_proto + +The encryption algorithm for SNMPv3 messages that require privacy (`user.priv_proto`): + +| String value | Int value | Description | +|:------------:|:---------:|-------------------------------------------------------------------------| +| none | 1 | no message encryption | +| des | 2 | ES encryption (CBC-DES) | +| aes | 3 | 128-bit AES encryption (CFB-AES-128) | +| aes192 | 4 | 192-bit AES encryption (CFB-AES-192) with "Blumenthal" key localization | +| aes256 | 5 | 256-bit AES encryption (CFB-AES-256) with "Blumenthal" key localization | +| aes192c | 6 | 192-bit AES encryption (CFB-AES-192) with "Reeder" key localization | +| aes256c | 7 | 256-bit AES encryption (CFB-AES-256) with "Reeder" key localization | + + +</details> + +#### Examples + +##### SNMPv1/2 + +In this example: + +- the SNMP device is `192.0.2.1`. +- the SNMP version is `2`. +- the SNMP community is `public`. +- we will update the values every 10 seconds. +- we define 2 charts `bandwidth_port1` and `bandwidth_port2`, each having 2 dimensions: `in` and `out`. + +> **SNMPv1**: just set `options.version` to 1. +> **Note**: the algorithm chosen is `incremental`, because the collected values show the total number of bytes transferred, which we need to transform into kbps. To chart gauges (e.g. temperature), use `absolute` instead. + + +<details><summary>Config</summary> + +```yaml +jobs: + - name: switch + update_every: 10 + hostname: 192.0.2.1 + community: public + options: + version: 2 + charts: + - id: "bandwidth_port1" + title: "Switch Bandwidth for port 1" + units: "kilobits/s" + type: "area" + family: "ports" + dimensions: + - name: "in" + oid: "1.3.6.1.2.1.2.2.1.10.1" + algorithm: "incremental" + multiplier: 8 + divisor: 1000 + - name: "out" + oid: "1.3.6.1.2.1.2.2.1.16.1" + multiplier: -8 + divisor: 1000 + - id: "bandwidth_port2" + title: "Switch Bandwidth for port 2" + units: "kilobits/s" + type: "area" + family: "ports" + dimensions: + - name: "in" + oid: "1.3.6.1.2.1.2.2.1.10.2" + algorithm: "incremental" + multiplier: 8 + divisor: 1000 + - name: "out" + oid: "1.3.6.1.2.1.2.2.1.16.2" + multiplier: -8 + divisor: 1000 + +``` +</details> + +##### SNMPv3 + +To use SNMPv3: + +- use `user` instead of `community`. +- set `options.version` to 3. + +The rest of the configuration is the same as in the SNMPv1/2 example. + + +<details><summary>Config</summary> + +```yaml +jobs: + - name: switch + update_every: 10 + hostname: 192.0.2.1 + options: + version: 3 + user: + name: username + level: authPriv + auth_proto: sha256 + auth_key: auth_protocol_passphrase + priv_proto: aes256 + priv_key: priv_protocol_passphrase + +``` +</details> + +##### Multiply range + +If you need to define many charts using incremental OIDs, you can use the `charts.multiply_range` option. + +This is like the SNMPv1/2 example, but the option will multiply the current chart from 1 to 24 inclusive, producing 24 charts in total for the 24 ports of the switch `192.0.2.1`. + +Each of the 24 new charts will have its id (1-24) appended at: + +- its chart unique `id`, i.e. `bandwidth_port_1` to `bandwidth_port_24`. +- its title, i.e. `Switch Bandwidth for port 1` to `Switch Bandwidth for port 24`. +- its `oid` (for all dimensions), i.e. dimension in will be `1.3.6.1.2.1.2.2.1.10.1` to `1.3.6.1.2.1.2.2.1.10.24`. +- its `priority` will be incremented for each chart so that the charts will appear on the dashboard in this order. + + +<details><summary>Config</summary> + +```yaml +jobs: + - name: switch + update_every: 10 + hostname: "192.0.2.1" + community: public + options: + version: 2 + charts: + - id: "bandwidth_port" + title: "Switch Bandwidth for port" + units: "kilobits/s" + type: "area" + family: "ports" + multiply_range: [1, 24] + dimensions: + - name: "in" + oid: "1.3.6.1.2.1.2.2.1.10" + algorithm: "incremental" + multiplier: 8 + divisor: 1000 + - name: "out" + oid: "1.3.6.1.2.1.2.2.1.16" + multiplier: -8 + divisor: 1000 + +``` +</details> + +##### Multiple devices with a common configuration + +YAML supports [anchors](https://yaml.org/spec/1.2.2/#3222-anchors-and-aliases). +The `&` defines and names an anchor, and the `*` uses it. `<<: *anchor` means, inject the anchor, then extend. We can use anchors to share the common configuration for multiple devices. + +The following example: + +- adds an `anchor` to the first job. +- injects (copies) the first job configuration to the second and updates `name` and `hostname` parameters. +- injects (copies) the first job configuration to the third and updates `name` and `hostname` parameters. + + +<details><summary>Config</summary> + +```yaml +jobs: + - &anchor + name: switch + update_every: 10 + hostname: "192.0.2.1" + community: public + options: + version: 2 + charts: + - id: "bandwidth_port1" + title: "Switch Bandwidth for port 1" + units: "kilobits/s" + type: "area" + family: "ports" + dimensions: + - name: "in" + oid: "1.3.6.1.2.1.2.2.1.10.1" + algorithm: "incremental" + multiplier: 8 + divisor: 1000 + - name: "out" + oid: "1.3.6.1.2.1.2.2.1.16.1" + multiplier: -8 + divisor: 1000 + - <<: *anchor + name: switch2 + hostname: "192.0.2.2" + - <<: *anchor + name: switch3 + hostname: "192.0.2.3" + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +To troubleshoot issues with the `snmp` collector, run the `go.d.plugin` with the debug option enabled. The output +should give you clues as to why the collector isn't working. + +- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on + your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`. + + ```bash + cd /usr/libexec/netdata/plugins.d/ + ``` + +- Switch to the `netdata` user. + + ```bash + sudo -u netdata -s + ``` + +- Run the `go.d.plugin` to debug the collector: + + ```bash + ./go.d.plugin -d -m snmp + ``` + + diff --git a/src/go/collectors/go.d.plugin/modules/snmp/metadata.yaml b/src/go/collectors/go.d.plugin/modules/snmp/metadata.yaml new file mode 100644 index 000000000..a35b3190d --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/metadata.yaml @@ -0,0 +1,398 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-snmp + plugin_name: go.d.plugin + module_name: snmp + monitored_instance: + name: SNMP devices + link: "" + icon_filename: snmp.png + categories: + - data-collection.generic-data-collection + keywords: + - snmp + related_resources: + integrations: + list: [] + info_provided_to_referring_integrations: + description: "" + most_popular: true + overview: + data_collection: + metrics_description: | + This collector monitors any SNMP devices and uses the [gosnmp](https://github.com/gosnmp/gosnmp) package. + + It supports: + + - all SNMP versions: SNMPv1, SNMPv2c and SNMPv3. + - any number of SNMP devices. + - each SNMP device can be used to collect data for any number of charts. + - each chart may have any number of dimensions. + - each SNMP device may have a different update frequency. + - each SNMP device will accept one or more batches to report values (you can set `max_request_size` per SNMP server, to control the size of batches). + + Keep in mind that many SNMP switches and routers are very slow. They may not be able to report values per second. + `go.d.plugin` reports the time it took for the SNMP device to respond when executed in the debug mode. + + Also, if many SNMP clients are used on the same SNMP device at the same time, values may be skipped. + This is a problem of the SNMP device, not this collector. In this case, consider reducing the frequency of data collection (increasing `update_every`). + method_description: "" + supported_platforms: + include: [] + exclude: [] + multi_instance: true + additional_permissions: + description: "" + default_behavior: + auto_detection: + description: "" + limits: + description: "" + performance_impact: + description: "" + setup: + prerequisites: + list: + - title: Find OIDs + description: | + Use `snmpwalk`, like this: + + ```sh + snmpwalk -t 20 -O fn -v 2c -c public 192.0.2.1 + ``` + + - `-t 20` is the timeout in seconds. + - `-O fn` will display full OIDs in numeric format. + - `-v 2c` is the SNMP version. + - `-c public` is the SNMP community. + - `192.0.2.1` is the SNMP device. + configuration: + file: + name: go.d/snmp.conf + options: + description: | + The following options can be defined globally: update_every, autodetection_retry. + folding: + title: Config options + enabled: true + list: + - name: update_every + description: Data collection frequency. + default_value: 1 + required: false + - name: autodetection_retry + description: Recheck interval in seconds. Zero means no recheck will be scheduled. + default_value: 0 + required: false + - name: hostname + description: Target ipv4 address. + default_value: 127.0.0.1 + required: true + - name: community + description: SNMPv1/2 community string. + default_value: public + required: false + - name: options.version + description: "SNMP version. Available versions: 1, 2, 3." + default_value: 2 + required: false + - name: options.port + description: Target port. + default_value: 161 + required: false + - name: options.retries + description: Retries to attempt. + default_value: 1 + required: false + - name: options.timeout + description: SNMP request/response timeout. + default_value: 10 + required: false + - name: options.max_request_size + description: Maximum number of OIDs allowed in one one SNMP request. + default_value: 60 + required: false + - name: user.name + description: SNMPv3 user name. + default_value: "" + required: false + - name: user.name + description: Security level of SNMPv3 messages. + default_value: "" + required: false + - name: user.auth_proto + description: Security level of SNMPv3 messages. + default_value: "" + required: false + detailed_description: | + The security of an SNMPv3 message as per RFC 3414 (`user.level`): + + | String value | Int value | Description | + |:------------:|:---------:|------------------------------------------| + | none | 1 | no message authentication or encryption | + | authNoPriv | 2 | message authentication and no encryption | + | authPriv | 3 | message authentication and encryption | + - name: user.name + description: Authentication protocol for SNMPv3 messages. + default_value: "" + required: false + detailed_description: | + The digest algorithm for SNMPv3 messages that require authentication (`user.auth_proto`): + + | String value | Int value | Description | + |:------------:|:---------:|-------------------------------------------| + | none | 1 | no message authentication | + | md5 | 2 | MD5 message authentication (HMAC-MD5-96) | + | sha | 3 | SHA message authentication (HMAC-SHA-96) | + | sha224 | 4 | SHA message authentication (HMAC-SHA-224) | + | sha256 | 5 | SHA message authentication (HMAC-SHA-256) | + | sha384 | 6 | SHA message authentication (HMAC-SHA-384) | + | sha512 | 7 | SHA message authentication (HMAC-SHA-512) | + - name: user.auth_key + description: Authentication protocol pass phrase. + default_value: "" + required: false + - name: user.priv_proto + description: Privacy protocol for SNMPv3 messages. + default_value: "" + required: false + detailed_description: | + The encryption algorithm for SNMPv3 messages that require privacy (`user.priv_proto`): + + | String value | Int value | Description | + |:------------:|:---------:|-------------------------------------------------------------------------| + | none | 1 | no message encryption | + | des | 2 | ES encryption (CBC-DES) | + | aes | 3 | 128-bit AES encryption (CFB-AES-128) | + | aes192 | 4 | 192-bit AES encryption (CFB-AES-192) with "Blumenthal" key localization | + | aes256 | 5 | 256-bit AES encryption (CFB-AES-256) with "Blumenthal" key localization | + | aes192c | 6 | 192-bit AES encryption (CFB-AES-192) with "Reeder" key localization | + | aes256c | 7 | 256-bit AES encryption (CFB-AES-256) with "Reeder" key localization | + - name: user.priv_key + description: Privacy protocol pass phrase. + default_value: "" + required: false + - name: charts + description: List of charts. + default_value: "[]" + required: true + - name: charts.id + description: Chart ID. Used to uniquely identify the chart. + default_value: "" + required: true + - name: charts.title + description: Chart title. + default_value: "Untitled chart" + required: false + - name: charts.units + description: Chart units. + default_value: num + required: false + - name: charts.family + description: Chart family. + default_value: charts.id + required: false + - name: charts.type + description: Chart type (line, area, stacked). + default_value: line + required: false + - name: charts.priority + description: Chart priority. + default_value: 70000 + required: false + - name: charts.multiply_range + description: Used when you need to define many charts using incremental OIDs. + default_value: "[]" + required: false + - name: charts.dimensions + description: List of chart dimensions. + default_value: "[]" + required: true + - name: charts.dimensions.oid + description: Collected metric OID. + default_value: "" + required: true + - name: charts.dimensions.name + description: Dimension name. + default_value: "" + required: true + - name: charts.dimensions.algorithm + description: Dimension algorithm (absolute, incremental). + default_value: absolute + required: false + - name: charts.dimensions.multiplier + description: Collected value multiplier, applied to convert it properly to units. + default_value: 1 + required: false + - name: charts.dimensions.divisor + description: Collected value divisor, applied to convert it properly to units. + default_value: 1 + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: SNMPv1/2 + description: | + In this example: + + - the SNMP device is `192.0.2.1`. + - the SNMP version is `2`. + - the SNMP community is `public`. + - we will update the values every 10 seconds. + - we define 2 charts `bandwidth_port1` and `bandwidth_port2`, each having 2 dimensions: `in` and `out`. + + > **SNMPv1**: just set `options.version` to 1. + > **Note**: the algorithm chosen is `incremental`, because the collected values show the total number of bytes transferred, which we need to transform into kbps. To chart gauges (e.g. temperature), use `absolute` instead. + config: | + jobs: + - name: switch + update_every: 10 + hostname: 192.0.2.1 + community: public + options: + version: 2 + charts: + - id: "bandwidth_port1" + title: "Switch Bandwidth for port 1" + units: "kilobits/s" + type: "area" + family: "ports" + dimensions: + - name: "in" + oid: "1.3.6.1.2.1.2.2.1.10.1" + algorithm: "incremental" + multiplier: 8 + divisor: 1000 + - name: "out" + oid: "1.3.6.1.2.1.2.2.1.16.1" + multiplier: -8 + divisor: 1000 + - id: "bandwidth_port2" + title: "Switch Bandwidth for port 2" + units: "kilobits/s" + type: "area" + family: "ports" + dimensions: + - name: "in" + oid: "1.3.6.1.2.1.2.2.1.10.2" + algorithm: "incremental" + multiplier: 8 + divisor: 1000 + - name: "out" + oid: "1.3.6.1.2.1.2.2.1.16.2" + multiplier: -8 + divisor: 1000 + - name: SNMPv3 + description: | + To use SNMPv3: + + - use `user` instead of `community`. + - set `options.version` to 3. + + The rest of the configuration is the same as in the SNMPv1/2 example. + config: | + jobs: + - name: switch + update_every: 10 + hostname: 192.0.2.1 + options: + version: 3 + user: + name: username + level: authPriv + auth_proto: sha256 + auth_key: auth_protocol_passphrase + priv_proto: aes256 + priv_key: priv_protocol_passphrase + - name: Multiply range + description: | + If you need to define many charts using incremental OIDs, you can use the `charts.multiply_range` option. + + This is like the SNMPv1/2 example, but the option will multiply the current chart from 1 to 24 inclusive, producing 24 charts in total for the 24 ports of the switch `192.0.2.1`. + + Each of the 24 new charts will have its id (1-24) appended at: + + - its chart unique `id`, i.e. `bandwidth_port_1` to `bandwidth_port_24`. + - its title, i.e. `Switch Bandwidth for port 1` to `Switch Bandwidth for port 24`. + - its `oid` (for all dimensions), i.e. dimension in will be `1.3.6.1.2.1.2.2.1.10.1` to `1.3.6.1.2.1.2.2.1.10.24`. + - its `priority` will be incremented for each chart so that the charts will appear on the dashboard in this order. + config: | + jobs: + - name: switch + update_every: 10 + hostname: "192.0.2.1" + community: public + options: + version: 2 + charts: + - id: "bandwidth_port" + title: "Switch Bandwidth for port" + units: "kilobits/s" + type: "area" + family: "ports" + multiply_range: [1, 24] + dimensions: + - name: "in" + oid: "1.3.6.1.2.1.2.2.1.10" + algorithm: "incremental" + multiplier: 8 + divisor: 1000 + - name: "out" + oid: "1.3.6.1.2.1.2.2.1.16" + multiplier: -8 + divisor: 1000 + - name: Multiple devices with a common configuration + description: | + YAML supports [anchors](https://yaml.org/spec/1.2.2/#3222-anchors-and-aliases). + The `&` defines and names an anchor, and the `*` uses it. `<<: *anchor` means, inject the anchor, then extend. We can use anchors to share the common configuration for multiple devices. + + The following example: + + - adds an `anchor` to the first job. + - injects (copies) the first job configuration to the second and updates `name` and `hostname` parameters. + - injects (copies) the first job configuration to the third and updates `name` and `hostname` parameters. + config: | + jobs: + - &anchor + name: switch + update_every: 10 + hostname: "192.0.2.1" + community: public + options: + version: 2 + charts: + - id: "bandwidth_port1" + title: "Switch Bandwidth for port 1" + units: "kilobits/s" + type: "area" + family: "ports" + dimensions: + - name: "in" + oid: "1.3.6.1.2.1.2.2.1.10.1" + algorithm: "incremental" + multiplier: 8 + divisor: 1000 + - name: "out" + oid: "1.3.6.1.2.1.2.2.1.16.1" + multiplier: -8 + divisor: 1000 + - <<: *anchor + name: switch2 + hostname: "192.0.2.2" + - <<: *anchor + name: switch3 + hostname: "192.0.2.3" + troubleshooting: + problems: + list: [] + alerts: [] + metrics: + folding: + title: Metrics + enabled: false + description: The metrics that will be collected are defined in the configuration file. + availability: [] + scopes: [] diff --git a/src/go/collectors/go.d.plugin/modules/snmp/snmp.go b/src/go/collectors/go.d.plugin/modules/snmp/snmp.go new file mode 100644 index 000000000..e42299336 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/snmp.go @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package snmp + +import ( + _ "embed" + "errors" + "fmt" + "strings" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + + "github.com/gosnmp/gosnmp" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("snmp", module.Creator{ + JobConfigSchema: configSchema, + Defaults: module.Defaults{ + UpdateEvery: defaultUpdateEvery, + }, + Create: func() module.Module { return New() }, + }) +} + +const ( + defaultUpdateEvery = 10 + defaultHostname = "127.0.0.1" + defaultCommunity = "public" + defaultVersion = gosnmp.Version2c + defaultPort = 161 + defaultRetries = 1 + defaultTimeout = defaultUpdateEvery + defaultMaxOIDs = 60 +) + +func New() *SNMP { + return &SNMP{ + Config: Config{ + Hostname: defaultHostname, + Community: defaultCommunity, + Options: Options{ + Port: defaultPort, + Retries: defaultRetries, + Timeout: defaultUpdateEvery, + Version: defaultVersion.String(), + MaxOIDs: defaultMaxOIDs, + }, + User: User{ + Name: "", + SecurityLevel: "authPriv", + AuthProto: "sha512", + AuthKey: "", + PrivProto: "aes192c", + PrivKey: "", + }, + }, + } +} + +type ( + Config struct { + UpdateEvery int `yaml:"update_every" json:"update_every"` + Hostname string `yaml:"hostname" json:"hostname"` + Community string `yaml:"community" json:"community"` + User User `yaml:"user" json:"user"` + Options Options `yaml:"options" json:"options"` + ChartsInput []ChartConfig `yaml:"charts" json:"charts"` + } + User struct { + Name string `yaml:"name" json:"name"` + SecurityLevel string `yaml:"level" json:"level"` + AuthProto string `yaml:"auth_proto" json:"auth_proto"` + AuthKey string `yaml:"auth_key" json:"auth_key"` + PrivProto string `yaml:"priv_proto" json:"priv_proto"` + PrivKey string `yaml:"priv_key" json:"priv_key"` + } + Options struct { + Port int `yaml:"port" json:"port"` + Retries int `yaml:"retries" json:"retries"` + Timeout int `yaml:"timeout" json:"timeout"` + Version string `yaml:"version" json:"version"` + MaxOIDs int `yaml:"max_request_size" json:"max_request_size"` + } + ChartConfig struct { + ID string `yaml:"id" json:"id"` + Title string `yaml:"title" json:"title"` + Units string `yaml:"units" json:"units"` + Family string `yaml:"family" json:"family"` + Type string `yaml:"type" json:"type"` + Priority int `yaml:"priority" json:"priority"` + IndexRange []int `yaml:"multiply_range" json:"multiply_range"` + Dimensions []DimensionConfig `yaml:"dimensions" json:"dimensions"` + } + DimensionConfig struct { + OID string `yaml:"oid" json:"oid"` + Name string `yaml:"name" json:"name"` + Algorithm string `yaml:"algorithm" json:"algorithm"` + Multiplier int `yaml:"multiplier" json:"multiplier"` + Divisor int `yaml:"divisor" json:"divisor"` + } +) + +type SNMP struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + snmpClient gosnmp.Handler + + oids []string +} + +func (s *SNMP) Configuration() any { + return s.Config +} + +func (s *SNMP) Init() error { + err := s.validateConfig() + if err != nil { + s.Errorf("config validation: %v", err) + return err + } + + snmpClient, err := s.initSNMPClient() + if err != nil { + s.Errorf("SNMP client initialization: %v", err) + return err + } + + s.Info(snmpClientConnInfo(snmpClient)) + + err = snmpClient.Connect() + if err != nil { + s.Errorf("SNMP client connect: %v", err) + return err + } + s.snmpClient = snmpClient + + charts, err := newCharts(s.ChartsInput) + if err != nil { + s.Errorf("Population of charts failed: %v", err) + return err + } + s.charts = charts + + s.oids = s.initOIDs() + + return nil +} + +func (s *SNMP) Check() error { + mx, err := s.collect() + if err != nil { + s.Error(err) + return err + } + if len(mx) == 0 { + return errors.New("no metrics collected") + } + return nil +} + +func (s *SNMP) Charts() *module.Charts { + return s.charts +} + +func (s *SNMP) Collect() map[string]int64 { + mx, err := s.collect() + if err != nil { + s.Error(err) + } + + if len(mx) == 0 { + return nil + } + return mx +} + +func (s *SNMP) Cleanup() { + if s.snmpClient != nil { + _ = s.snmpClient.Close() + } +} + +func snmpClientConnInfo(c gosnmp.Handler) string { + var info strings.Builder + info.WriteString(fmt.Sprintf("hostname=%s,port=%d,snmp_version=%s", c.Target(), c.Port(), c.Version())) + switch c.Version() { + case gosnmp.Version1, gosnmp.Version2c: + info.WriteString(fmt.Sprintf(",community=%s", c.Community())) + case gosnmp.Version3: + info.WriteString(fmt.Sprintf(",security_level=%d,%s", c.MsgFlags(), c.SecurityParameters().Description())) + } + return info.String() +} diff --git a/src/go/collectors/go.d.plugin/modules/snmp/snmp_test.go b/src/go/collectors/go.d.plugin/modules/snmp/snmp_test.go new file mode 100644 index 000000000..04d9db3f9 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/snmp_test.go @@ -0,0 +1,520 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package snmp + +import ( + "errors" + "fmt" + "os" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/gosnmp/gosnmp" + snmpmock "github.com/gosnmp/gosnmp/mocks" + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + } { + require.NotNil(t, data, name) + } +} + +func TestSNMP_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &SNMP{}, dataConfigJSON, dataConfigYAML) +} + +func TestSNMP_Init(t *testing.T) { + tests := map[string]struct { + prepareSNMP func() *SNMP + wantFail bool + }{ + "fail with default config": { + wantFail: true, + prepareSNMP: func() *SNMP { + return New() + }, + }, + "fail when 'charts' not set": { + wantFail: true, + prepareSNMP: func() *SNMP { + snmp := New() + snmp.Config = prepareV2Config() + snmp.ChartsInput = nil + return snmp + }, + }, + "fail when using SNMPv3 but 'user.name' not set": { + wantFail: true, + prepareSNMP: func() *SNMP { + snmp := New() + snmp.Config = prepareV3Config() + snmp.User.Name = "" + return snmp + }, + }, + "fail when using SNMPv3 but 'user.level' is invalid": { + wantFail: true, + prepareSNMP: func() *SNMP { + snmp := New() + snmp.Config = prepareV3Config() + snmp.User.SecurityLevel = "invalid" + return snmp + }, + }, + "fail when using SNMPv3 but 'user.auth_proto' is invalid": { + wantFail: true, + prepareSNMP: func() *SNMP { + snmp := New() + snmp.Config = prepareV3Config() + snmp.User.AuthProto = "invalid" + return snmp + }, + }, + "fail when using SNMPv3 but 'user.priv_proto' is invalid": { + wantFail: true, + prepareSNMP: func() *SNMP { + snmp := New() + snmp.Config = prepareV3Config() + snmp.User.PrivProto = "invalid" + return snmp + }, + }, + "success when using SNMPv1 with valid config": { + wantFail: false, + prepareSNMP: func() *SNMP { + snmp := New() + snmp.Config = prepareV1Config() + return snmp + }, + }, + "success when using SNMPv2 with valid config": { + wantFail: false, + prepareSNMP: func() *SNMP { + snmp := New() + snmp.Config = prepareV2Config() + return snmp + }, + }, + "success when using SNMPv3 with valid config": { + wantFail: false, + prepareSNMP: func() *SNMP { + snmp := New() + snmp.Config = prepareV3Config() + return snmp + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + snmp := test.prepareSNMP() + + if test.wantFail { + assert.Error(t, snmp.Init()) + } else { + assert.NoError(t, snmp.Init()) + } + }) + } +} + +func TestSNMP_Check(t *testing.T) { + tests := map[string]struct { + prepareSNMP func(m *snmpmock.MockHandler) *SNMP + wantFail bool + }{ + "success when 'max_request_size' > returned OIDs": { + wantFail: false, + prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareV2Config() + + m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ + Variables: []gosnmp.SnmpPDU{ + {Value: 10, Type: gosnmp.Gauge32}, + {Value: 20, Type: gosnmp.Gauge32}, + }, + }, nil).Times(1) + + return snmp + }, + }, + "success when 'max_request_size' < returned OIDs": { + wantFail: false, + prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareV2Config() + snmp.Config.Options.MaxOIDs = 1 + + m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ + Variables: []gosnmp.SnmpPDU{ + {Value: 10, Type: gosnmp.Gauge32}, + {Value: 20, Type: gosnmp.Gauge32}, + }, + }, nil).Times(2) + + return snmp + }, + }, + "success when using 'multiply_range'": { + wantFail: false, + prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareConfigWithIndexRange(prepareV2Config, 0, 1) + + m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ + Variables: []gosnmp.SnmpPDU{ + {Value: 10, Type: gosnmp.Gauge32}, + {Value: 20, Type: gosnmp.Gauge32}, + {Value: 30, Type: gosnmp.Gauge32}, + {Value: 40, Type: gosnmp.Gauge32}, + }, + }, nil).Times(1) + + return snmp + }, + }, + "fail when snmp client Get fails": { + wantFail: true, + prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareV2Config() + + m.EXPECT().Get(gomock.Any()).Return(nil, errors.New("mock Get() error")).Times(1) + + return snmp + }, + }, + "fail when all OIDs type is unsupported": { + wantFail: true, + prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareV2Config() + + m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ + Variables: []gosnmp.SnmpPDU{ + {Value: nil, Type: gosnmp.NoSuchInstance}, + {Value: nil, Type: gosnmp.NoSuchInstance}, + }, + }, nil).Times(1) + + return snmp + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockSNMP, cleanup := mockInit(t) + defer cleanup() + + newSNMPClient = func() gosnmp.Handler { return mockSNMP } + defaultMockExpects(mockSNMP) + + snmp := test.prepareSNMP(mockSNMP) + require.NoError(t, snmp.Init()) + + if test.wantFail { + assert.Error(t, snmp.Check()) + } else { + assert.NoError(t, snmp.Check()) + } + }) + } +} + +func TestSNMP_Collect(t *testing.T) { + tests := map[string]struct { + prepareSNMP func(m *snmpmock.MockHandler) *SNMP + wantCollected map[string]int64 + }{ + "success when collecting supported type": { + prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareConfigWithIndexRange(prepareV2Config, 0, 3) + + m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ + Variables: []gosnmp.SnmpPDU{ + {Value: 10, Type: gosnmp.Counter32}, + {Value: 20, Type: gosnmp.Counter64}, + {Value: 30, Type: gosnmp.Gauge32}, + {Value: 1, Type: gosnmp.Boolean}, + {Value: 40, Type: gosnmp.Gauge32}, + {Value: 50, Type: gosnmp.TimeTicks}, + {Value: 60, Type: gosnmp.Uinteger32}, + {Value: 70, Type: gosnmp.Integer}, + }, + }, nil).Times(1) + + return snmp + }, + wantCollected: map[string]int64{ + "1.3.6.1.2.1.2.2.1.10.0": 10, + "1.3.6.1.2.1.2.2.1.16.0": 20, + "1.3.6.1.2.1.2.2.1.10.1": 30, + "1.3.6.1.2.1.2.2.1.16.1": 1, + "1.3.6.1.2.1.2.2.1.10.2": 40, + "1.3.6.1.2.1.2.2.1.16.2": 50, + "1.3.6.1.2.1.2.2.1.10.3": 60, + "1.3.6.1.2.1.2.2.1.16.3": 70, + }, + }, + "success when collecting supported and unsupported type": { + prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareConfigWithIndexRange(prepareV2Config, 0, 2) + + m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ + Variables: []gosnmp.SnmpPDU{ + {Value: 10, Type: gosnmp.Counter32}, + {Value: 20, Type: gosnmp.Counter64}, + {Value: 30, Type: gosnmp.Gauge32}, + {Value: nil, Type: gosnmp.NoSuchInstance}, + {Value: nil, Type: gosnmp.NoSuchInstance}, + {Value: nil, Type: gosnmp.NoSuchInstance}, + }, + }, nil).Times(1) + + return snmp + }, + wantCollected: map[string]int64{ + "1.3.6.1.2.1.2.2.1.10.0": 10, + "1.3.6.1.2.1.2.2.1.16.0": 20, + "1.3.6.1.2.1.2.2.1.10.1": 30, + }, + }, + "fails when collecting unsupported type": { + prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareConfigWithIndexRange(prepareV2Config, 0, 2) + + m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ + Variables: []gosnmp.SnmpPDU{ + {Value: nil, Type: gosnmp.NoSuchInstance}, + {Value: nil, Type: gosnmp.NoSuchInstance}, + {Value: nil, Type: gosnmp.NoSuchObject}, + {Value: "192.0.2.0", Type: gosnmp.NsapAddress}, + {Value: []uint8{118, 101, 116}, Type: gosnmp.OctetString}, + {Value: ".1.3.6.1.2.1.4.32.1.5.2.1.4.10.19.0.0.16", Type: gosnmp.ObjectIdentifier}, + }, + }, nil).Times(1) + + return snmp + }, + wantCollected: nil, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockSNMP, cleanup := mockInit(t) + defer cleanup() + + newSNMPClient = func() gosnmp.Handler { return mockSNMP } + defaultMockExpects(mockSNMP) + + snmp := test.prepareSNMP(mockSNMP) + require.NoError(t, snmp.Init()) + + collected := snmp.Collect() + + assert.Equal(t, test.wantCollected, collected) + }) + } +} + +func TestSNMP_Cleanup(t *testing.T) { + tests := map[string]struct { + prepareSNMP func(t *testing.T, m *snmpmock.MockHandler) *SNMP + }{ + "cleanup call if snmpClient initialized": { + prepareSNMP: func(t *testing.T, m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareV2Config() + require.NoError(t, snmp.Init()) + + m.EXPECT().Close().Times(1) + + return snmp + }, + }, + "cleanup call does not panic if snmpClient not initialized": { + prepareSNMP: func(t *testing.T, m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareV2Config() + require.NoError(t, snmp.Init()) + snmp.snmpClient = nil + + return snmp + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockSNMP, cleanup := mockInit(t) + defer cleanup() + + newSNMPClient = func() gosnmp.Handler { return mockSNMP } + defaultMockExpects(mockSNMP) + + snmp := test.prepareSNMP(t, mockSNMP) + assert.NotPanics(t, snmp.Cleanup) + }) + } +} + +func TestSNMP_Charts(t *testing.T) { + tests := map[string]struct { + prepareSNMP func(t *testing.T, m *snmpmock.MockHandler) *SNMP + wantNumCharts int + }{ + "without 'multiply_range': got expected number of charts": { + wantNumCharts: 1, + prepareSNMP: func(t *testing.T, m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareV2Config() + require.NoError(t, snmp.Init()) + + return snmp + }, + }, + "with 'multiply_range': got expected number of charts": { + wantNumCharts: 10, + prepareSNMP: func(t *testing.T, m *snmpmock.MockHandler) *SNMP { + snmp := New() + snmp.Config = prepareConfigWithIndexRange(prepareV2Config, 0, 9) + require.NoError(t, snmp.Init()) + + return snmp + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockSNMP, cleanup := mockInit(t) + defer cleanup() + + newSNMPClient = func() gosnmp.Handler { return mockSNMP } + defaultMockExpects(mockSNMP) + + snmp := test.prepareSNMP(t, mockSNMP) + assert.Equal(t, test.wantNumCharts, len(*snmp.Charts())) + }) + } +} + +func mockInit(t *testing.T) (*snmpmock.MockHandler, func()) { + mockCtl := gomock.NewController(t) + cleanup := func() { mockCtl.Finish() } + mockSNMP := snmpmock.NewMockHandler(mockCtl) + + return mockSNMP, cleanup +} + +func defaultMockExpects(m *snmpmock.MockHandler) { + m.EXPECT().Target().AnyTimes() + m.EXPECT().Port().AnyTimes() + m.EXPECT().Retries().AnyTimes() + m.EXPECT().Timeout().AnyTimes() + m.EXPECT().MaxOids().AnyTimes() + m.EXPECT().Version().AnyTimes() + m.EXPECT().Community().AnyTimes() + m.EXPECT().SetTarget(gomock.Any()).AnyTimes() + m.EXPECT().SetPort(gomock.Any()).AnyTimes() + m.EXPECT().SetRetries(gomock.Any()).AnyTimes() + m.EXPECT().SetMaxOids(gomock.Any()).AnyTimes() + m.EXPECT().SetLogger(gomock.Any()).AnyTimes() + m.EXPECT().SetTimeout(gomock.Any()).AnyTimes() + m.EXPECT().SetCommunity(gomock.Any()).AnyTimes() + m.EXPECT().SetVersion(gomock.Any()).AnyTimes() + m.EXPECT().SetSecurityModel(gomock.Any()).AnyTimes() + m.EXPECT().SetMsgFlags(gomock.Any()).AnyTimes() + m.EXPECT().SetSecurityParameters(gomock.Any()).AnyTimes() + m.EXPECT().Connect().Return(nil).AnyTimes() +} + +func prepareConfigWithIndexRange(p func() Config, start, end int) Config { + if start > end || start < 0 || end < 1 { + panic(fmt.Sprintf("invalid index range ('%d'-'%d')", start, end)) + } + cfg := p() + for i := range cfg.ChartsInput { + cfg.ChartsInput[i].IndexRange = []int{start, end} + } + return cfg +} + +func prepareV3Config() Config { + cfg := prepareV2Config() + cfg.Options.Version = gosnmp.Version3.String() + cfg.User = User{ + Name: "name", + SecurityLevel: "authPriv", + AuthProto: strings.ToLower(gosnmp.MD5.String()), + AuthKey: "auth_key", + PrivProto: strings.ToLower(gosnmp.AES.String()), + PrivKey: "priv_key", + } + return cfg +} + +func prepareV2Config() Config { + cfg := prepareV1Config() + cfg.Options.Version = gosnmp.Version2c.String() + return cfg +} + +func prepareV1Config() Config { + return Config{ + UpdateEvery: defaultUpdateEvery, + Hostname: defaultHostname, + Community: defaultCommunity, + Options: Options{ + Port: defaultPort, + Retries: defaultRetries, + Timeout: defaultTimeout, + Version: gosnmp.Version1.String(), + MaxOIDs: defaultMaxOIDs, + }, + ChartsInput: []ChartConfig{ + { + ID: "test_chart1", + Title: "This is Test Chart1", + Units: "kilobits/s", + Family: "family", + Type: module.Area.String(), + Priority: module.Priority, + Dimensions: []DimensionConfig{ + { + OID: "1.3.6.1.2.1.2.2.1.10", + Name: "in", + Algorithm: module.Incremental.String(), + Multiplier: 8, + Divisor: 1000, + }, + { + OID: "1.3.6.1.2.1.2.2.1.16", + Name: "out", + Algorithm: module.Incremental.String(), + Multiplier: 8, + Divisor: 1000, + }, + }, + }, + }, + } +} diff --git a/src/go/collectors/go.d.plugin/modules/snmp/testdata/config.json b/src/go/collectors/go.d.plugin/modules/snmp/testdata/config.json new file mode 100644 index 000000000..c0fff4868 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/testdata/config.json @@ -0,0 +1,42 @@ +{ + "update_every": 123, + "hostname": "ok", + "community": "ok", + "user": { + "name": "ok", + "level": "ok", + "auth_proto": "ok", + "auth_key": "ok", + "priv_proto": "ok", + "priv_key": "ok" + }, + "options": { + "port": 123, + "retries": 123, + "timeout": 123, + "version": "ok", + "max_request_size": 123 + }, + "charts": [ + { + "id": "ok", + "title": "ok", + "units": "ok", + "family": "ok", + "type": "ok", + "priority": 123, + "multiply_range": [ + 123 + ], + "dimensions": [ + { + "oid": "ok", + "name": "ok", + "algorithm": "ok", + "multiplier": 123, + "divisor": 123 + } + ] + } + ] +} diff --git a/src/go/collectors/go.d.plugin/modules/snmp/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/snmp/testdata/config.yaml new file mode 100644 index 000000000..98620fb9c --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/snmp/testdata/config.yaml @@ -0,0 +1,31 @@ +update_every: 123 +hostname: "ok" +community: "ok" +user: + name: "ok" + level: "ok" + auth_proto: "ok" + auth_key: "ok" + priv_proto: "ok" + priv_key: "ok" +options: + port: 123 + retries: 123 + timeout: 123 + version: "ok" + max_request_size: 123 +charts: + - id: "ok" + title: "ok" + units: "ok" + family: "ok" + type: "ok" + priority: 123 + multiply_range: + - 123 + dimensions: + - oid: "ok" + name: "ok" + algorithm: "ok" + multiplier: 123 + divisor: 123 |