diff --git a/README.md b/README.md index 9abd0e79..e15d0f2d 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,22 @@ Start `snmp_exporter` as a daemon or from CLI: ./snmp_exporter ``` -Visit [http://localhost:9116/snmp?target=192.0.0.8] where `192.0.0.8` is the IP or -FQDN of the SNMP device to get metrics from. Note that this will use the default auth (`public_v2`) and -default module (`if_mib`). The auth and module must be defined in the `snmp.yml`. +Visit where `192.0.0.8` is the IP or +FQDN of the SNMP device to get metrics from. Note that this will use the default transport (`udp`), +default port (`161`), default auth (`public_v2`) and default module (`if_mib`). The auth and module +must be defined in the `snmp.yml` file. For example, if you have an auth named `my_secure_v3` for walking `ddwrt`, the URL would look like -[http://localhost:9116/snmp?auth=my_secure_v3&module=ddwrt&target=192.0.0.8]. +. + +To configure a different transport and/or port, use the syntax `[transport://]host[:port]`. + +For example, to scrape a device using `tcp` on port `1161`, the URL would look like +. + +Note that [URL encoding](https://en.wikipedia.org/wiki/URL_encoding) should be used for `target` due +to the `:` and `/` characters. Prometheus encodes query parameters automatically and manual encoding +is not necessary within the Prometheus configuration file. ## Configuration @@ -85,7 +95,7 @@ The default configuration file name is `snmp.yml` and should not be edited by hand. If you need to change it, see [Generating configuration](#generating-configuration). -The default `snmp.yml` covers a variety of common hardware walking them +The default `snmp.yml` file covers a variety of common hardware walking them using SNMP v2 GETBULK. ## Prometheus Configuration @@ -100,6 +110,7 @@ scrape_configs: - targets: - 192.168.1.2 # SNMP device. - switch.local # SNMP device. + - tcp://192.168.1.3:1161 # SNMP device using TCP transport and custom port. metrics_path: /snmp params: auth: [public_v2] @@ -156,4 +167,4 @@ easier for others, please consider contributing back your configurations to us. `snmp.yml` config should be accompanied by generator config. For your dashboard, alerts, and recording rules, please consider -contributing them to https://github.com/prometheus/snmp_exporter/tree/main/snmp-mixin +contributing them to . diff --git a/collector/collector.go b/collector/collector.go index 1879bf39..1b62288b 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -117,15 +117,9 @@ func ScrapeTarget(ctx context.Context, target string, auth *config.Auth, module results.retries++ } - snmp.Target = target - snmp.Port = 161 - if host, port, err := net.SplitHostPort(target); err == nil { - snmp.Target = host - p, err := strconv.Atoi(port) - if err != nil { - return results, fmt.Errorf("error converting port number to int for target %s: %s", target, err) - } - snmp.Port = uint16(p) + // Configure target. + if err := configureTarget(&snmp, target); err != nil { + return results, err } // Configure auth. @@ -242,6 +236,24 @@ func ScrapeTarget(ctx context.Context, target string, auth *config.Auth, module return results, nil } +func configureTarget(g *gosnmp.GoSNMP, target string) error { + if s := strings.SplitN(target, "://", 2); len(s) == 2 { + g.Transport = s[0] + target = s[1] + } + g.Target = target + g.Port = 161 + if host, port, err := net.SplitHostPort(target); err == nil { + g.Target = host + p, err := strconv.Atoi(port) + if err != nil { + return fmt.Errorf("error converting port number to int for target %q: %w", target, err) + } + g.Port = uint16(p) + } + return nil +} + func filterAllowedIndices(logger log.Logger, filter config.DynamicFilter, pdus []gosnmp.SnmpPDU, allowedList []string, metrics internalMetrics) []string { level.Debug(logger).Log("msg", "Evaluating rule for oid", "oid", filter.Oid) for _, pdu := range pdus { diff --git a/collector/collector_test.go b/collector/collector_test.go index a1a009d8..818a8703 100644 --- a/collector/collector_test.go +++ b/collector/collector_test.go @@ -1033,6 +1033,173 @@ func TestIndexesToLabels(t *testing.T) { } } +func TestConfigureTarget(t *testing.T) { + cases := []struct { + target string + gTransport string + gTarget string + gPort uint16 + shouldErr bool + }{ + { + target: "localhost", + gTransport: "", + gTarget: "localhost", + gPort: 161, + shouldErr: false, + }, + { + target: "localhost:1161", + gTransport: "", + gTarget: "localhost", + gPort: 1161, + shouldErr: false, + }, + { + target: "udp://localhost", + gTransport: "udp", + gTarget: "localhost", + gPort: 161, + shouldErr: false, + }, + { + target: "udp://localhost:1161", + gTransport: "udp", + gTarget: "localhost", + gPort: 1161, + shouldErr: false, + }, + { + target: "tcp://localhost", + gTransport: "tcp", + gTarget: "localhost", + gPort: 161, + shouldErr: false, + }, + { + target: "tcp://localhost:1161", + gTransport: "tcp", + gTarget: "localhost", + gPort: 1161, + shouldErr: false, + }, + { + target: "[::1]", + gTransport: "", + gTarget: "[::1]", + gPort: 161, + shouldErr: false, + }, + { + target: "[::1]:1161", + gTransport: "", + gTarget: "::1", + gPort: 1161, + shouldErr: false, + }, + { + target: "udp://[::1]", + gTransport: "udp", + gTarget: "[::1]", + gPort: 161, + shouldErr: false, + }, + { + target: "udp://[::1]:1161", + gTransport: "udp", + gTarget: "::1", + gPort: 1161, + shouldErr: false, + }, + { + target: "tcp://[::1]", + gTransport: "tcp", + gTarget: "[::1]", + gPort: 161, + shouldErr: false, + }, + { + target: "tcp://[::1]:1161", + gTransport: "tcp", + gTarget: "::1", + gPort: 1161, + shouldErr: false, + }, + { // this case is valid during parse but invalid during connect + target: "tcp://udp://localhost:1161", + gTransport: "tcp", + gTarget: "udp://localhost:1161", + gPort: 161, + shouldErr: false, + }, + { + target: "localhost:badport", + gTransport: "", + gTarget: "", + gPort: 0, + shouldErr: true, + }, + { + target: "udp://localhost:badport", + gTransport: "", + gTarget: "", + gPort: 0, + shouldErr: true, + }, + { + target: "tcp://localhost:badport", + gTransport: "", + gTarget: "", + gPort: 0, + shouldErr: true, + }, + { + target: "[::1]:badport", + gTransport: "", + gTarget: "", + gPort: 0, + shouldErr: true, + }, + { + target: "udp://[::1]:badport", + gTransport: "", + gTarget: "", + gPort: 0, + shouldErr: true, + }, + { + target: "tcp://[::1]:badport", + gTransport: "", + gTarget: "", + gPort: 0, + shouldErr: true, + }, + } + + for _, c := range cases { + var g gosnmp.GoSNMP + err := configureTarget(&g, c.target) + if c.shouldErr { + if err == nil { + t.Fatalf("Was expecting error, but none returned for %q", c.target) + } + continue + } + if err != nil { + t.Fatalf("Error configuring target %q: %v", c.target, err) + } + if g.Transport != c.gTransport { + t.Fatalf("Bad SNMP transport for %q, got=%q, expected=%q", c.target, g.Transport, c.gTransport) + } + if g.Target != c.gTarget { + t.Fatalf("Bad SNMP target for %q, got=%q, expected=%q", c.target, g.Target, c.gTarget) + } + if g.Port != c.gPort { + t.Fatalf("Bad SNMP port for %q, got=%d, expected=%d", c.target, g.Port, c.gPort) + } + } +} + func TestFilterAllowedIndices(t *testing.T) { pdus := []gosnmp.SnmpPDU{