Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for parsing SNMP transport from target #914

Merged
merged 5 commits into from
Jul 15, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 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 Down Expand Up @@ -156,4 +166,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
111 changes: 111 additions & 0 deletions collector/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,117 @@ func TestIndexesToLabels(t *testing.T) {
}
}

func TestConfigureTarget(t *testing.T) {
hhromic marked this conversation as resolved.
Show resolved Hide resolved
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: "[::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,
},
{ // 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: "[::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