Skip to content

Commit

Permalink
Add support for parsing SNMP transport from target (#914)
Browse files Browse the repository at this point in the history
* Add support for parsing SNMP transport from target
* Syntax: `[transport://]host[:port]`

---------

Signed-off-by: Hugo Hromic <[email protected]>
  • Loading branch information
hhromic authored Jul 15, 2023
1 parent 636f1fa commit 03bc586
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 15 deletions.
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,30 @@ 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 <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 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].
<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
<http://localhost:9116/snmp?auth=my_secure_v3&module=ddwrt&target=tcp%3A%2F%2F192.0.0.8%3A1161>.

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

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
Expand All @@ -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]
Expand Down Expand Up @@ -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 <https://github.com/prometheus/snmp_exporter/tree/main/snmp-mixin>.
30 changes: 21 additions & 9 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
167 changes: 167 additions & 0 deletions collector/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down

0 comments on commit 03bc586

Please sign in to comment.