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

Split config of auth and modules #859

Merged
merged 2 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
## 0.23.0 / TBD

BREAKING CHANGES:

This version of the exporter introduces a new configuration file format. This
new format separates the walk and metric mappings from the connection and
authentication settings. This allows for easier configuration of different
auth params without having to duplicate the full walk and metric mapping.

See auth-split-migration.md for more details.

* [CHANGE] Split config of auth and modules #859

## 0.22.0 / 2023-06-15

* [FEATURE] Add indices filters #624
Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ This exporter is the recommended way to expose SNMP data in a format which
Prometheus can ingest.

To simply get started, it's recommended to use the `if_mib` module with
switches, access points, or routers.
switches, access points, or routers using the `public_v2` auth module,
which should be a read-only access community on the target device.

Note, community strings in SNMP are not considered secrets, as they are sent
unencrypted in SNMP v1 and v2c. For secure access, SNMP v3 is required.

# Concepts

Expand Down Expand Up @@ -68,9 +72,12 @@ Start `snmp_exporter` as a daemon or from CLI:
./snmp_exporter
```

Visit http://localhost:9116/snmp?module=if_mib&target=1.2.3.4 where `1.2.3.4` is the IP or
FQDN of the SNMP device to get metrics from and `if_mib` is the default module, defined
in `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 auth (`public_v2`) and
default module (`if_mib`). The auth and module must be defined in the `snmp.yml`.

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].

## Configuration

Expand All @@ -83,7 +90,7 @@ using SNMP v2 GETBULK.

## Prometheus Configuration

`target` and `module` can be passed as a parameter through relabelling.
The URL params `target`, `auth`, and `module` can be controlled through relabelling.

Example config:
```YAML
Expand All @@ -95,6 +102,7 @@ scrape_configs:
- switch.local # SNMP device.
metrics_path: /snmp
params:
auth: [public_v2]
module: [if_mib]
relabel_configs:
- source_labels: [__address__]
Expand Down
59 changes: 59 additions & 0 deletions auth-split-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Module and Auth Split Migration

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question I would like this to answer is how to sequence the change if you need a different auth than public_v2. Is it safe to specify the auth parameter with 0.22, then upgrade the exporter? Maybe this is obvious to experienced users of the exporter, but I'd rather make it explicit for the "someone else installed this 4 years ago and I need to keep it alive" target audience.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is already explicitly stated on line 3 that the config format in 0.23.0 has changed.

In <= 0.22.0 there was no top level modules: key, just a bare list of modules.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reworded line 3 slightly.


In [version 0.23.0](https://github.com/prometheus/snmp_exporter/releases/tag/v0.23.0) the configuration for the `snmp_epxorter` the configuration file format has changed. Configuration files for versions 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 configuratoins.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/configuratoins/configurations/


This change necessitates migration of the generator config and `snmp_exporter` config to the new format.

The complete `generator` format is [documented in generator/README.md#file-format](generator/README.md#file-format)

The complete `snmp_exporter` format is [documented in /generator/FORMAT.md](/generator/FORMAT.md).

## Examples

A generator containing the following config:

```yaml
modules:
sys_uptime:
version: 2
walk:
- sysUpTime
auth:
community: public
```

Would now become:

```yaml
auths:
public_v2:
community: public
version: 2
modules:
sys_uptime:
walk:
- sysUpTime
```

The newly generated `snmp_exporter` config would be:

```yaml
# WARNING: This file was auto-generated using snmp_exporter generator, manual changes will be lost.
auths:
public_v2:
community: public
security_level: noAuthNoPriv
auth_protocol: MD5
priv_protocol: DES
version: 2
modules:
if_mib:
get:
- 1.3.6.1.2.1.1.3.0
metrics:
- name: sysUpTime
oid: 1.3.6.1.2.1.1.3
type: gauge
help: The time (in hundredths of a second) since the network management portion
of the system was last re-initialized. - 1.3.6.1.2.1.1.3
```
29 changes: 15 additions & 14 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,19 @@ type ScrapeResults struct {
retries uint64
}

func ScrapeTarget(ctx context.Context, target string, config *config.Module, logger log.Logger) (ScrapeResults, error) {
func ScrapeTarget(ctx context.Context, target string, auth *config.Auth, module *config.Module, logger log.Logger) (ScrapeResults, error) {
results := ScrapeResults{}
// Set the options.
snmp := gosnmp.GoSNMP{}
snmp.Context = ctx
snmp.MaxRepetitions = config.WalkParams.MaxRepetitions
snmp.Retries = *config.WalkParams.Retries
snmp.Timeout = config.WalkParams.Timeout
snmp.UseUnconnectedUDPSocket = config.WalkParams.UseUnconnectedUDPSocket
snmp.MaxRepetitions = module.WalkParams.MaxRepetitions
snmp.Retries = *module.WalkParams.Retries
snmp.Timeout = module.WalkParams.Timeout
snmp.UseUnconnectedUDPSocket = module.WalkParams.UseUnconnectedUDPSocket
snmp.LocalAddr = *srcAddress

// Allow a set of OIDs that aren't in a strictly increasing order
if config.WalkParams.AllowNonIncreasingOIDs {
if module.WalkParams.AllowNonIncreasingOIDs {
snmp.AppOpts = make(map[string]interface{})
snmp.AppOpts["c"] = true
}
Expand Down Expand Up @@ -159,7 +159,7 @@ func ScrapeTarget(ctx context.Context, target string, config *config.Module, log
}

// Configure auth.
config.WalkParams.ConfigureSNMP(&snmp)
auth.ConfigureSNMP(&snmp)

// Do the actual walk.
getInitialStart := time.Now()
Expand All @@ -174,9 +174,9 @@ func ScrapeTarget(ctx context.Context, target string, config *config.Module, log
defer snmp.Conn.Close()

// Evaluate rules.
newGet := config.Get
newWalk := config.Walk
for _, filter := range config.Filters {
newGet := module.Get
newWalk := module.Walk
for _, filter := range module.Filters {
var pdus []gosnmp.SnmpPDU
allowedList := []string{}

Expand Down Expand Up @@ -206,7 +206,7 @@ func ScrapeTarget(ctx context.Context, target string, config *config.Module, log
}

getOids := newGet
maxOids := int(config.WalkParams.MaxRepetitions)
maxOids := int(module.WalkParams.MaxRepetitions)
// Max Repetition can be 0, maxOids cannot. SNMPv1 can only report one OID error per call.
if maxOids == 0 || snmp.Version == gosnmp.Version1 {
maxOids = 1
Expand Down Expand Up @@ -369,12 +369,13 @@ func buildMetricTree(metrics []*config.Metric) *MetricNode {
type collector struct {
ctx context.Context
target string
auth *config.Auth
module *config.Module
logger log.Logger
}

func New(ctx context.Context, target string, module *config.Module, logger log.Logger) *collector {
return &collector{ctx: ctx, target: target, module: module, logger: logger}
func New(ctx context.Context, target string, auth *config.Auth, module *config.Module, logger log.Logger) *collector {
return &collector{ctx: ctx, target: target, auth: auth, module: module, logger: logger}
}

// Describe implements Prometheus.Collector.
Expand All @@ -385,7 +386,7 @@ func (c collector) Describe(ch chan<- *prometheus.Desc) {
// Collect implements Prometheus.Collector.
func (c collector) Collect(ch chan<- prometheus.Metric) {
start := time.Now()
results, err := ScrapeTarget(c.ctx, c.target, c.module, c.logger)
results, err := ScrapeTarget(c.ctx, c.target, c.auth, c.module, c.logger)
if err != nil {
level.Info(c.logger).Log("msg", "Error scraping target", "err", err)
ch <- prometheus.NewInvalidMetric(prometheus.NewDesc("snmp_error", "Error scraping target", nil, nil), err)
Expand Down
107 changes: 61 additions & 46 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package config

import (
"errors"
"fmt"
"os"
"regexp"
Expand All @@ -33,6 +34,9 @@ func LoadFile(filename string) (*Config, error) {
if err != nil {
return nil, err
}
if cfg.Version != 2 {
return cfg, ErrUnsupportedVersion
}
return cfg, nil
}

Expand All @@ -44,13 +48,12 @@ var (
SecurityLevel: "noAuthNoPriv",
AuthProtocol: "MD5",
PrivProtocol: "DES",
Version: 2,
}
DefaultWalkParams = WalkParams{
Version: 2,
MaxRepetitions: 25,
Retries: &defaultRetries,
Timeout: time.Second * 5,
Auth: DefaultAuth,
UseUnconnectedUDPSocket: false,
AllowNonIncreasingOIDs: false,
}
Expand All @@ -60,17 +63,21 @@ var (
DefaultRegexpExtract = RegexpExtract{
Value: "$1",
}

ErrUnsupportedVersion = errors.New("unsupported config version")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ErrUnsupportedVersion = errors.New("unsupported config version")
ErrUnsupportedVersion = errors.New("Unsupported config version, we only support config version 2. Please see https://github.com/prometheus/snmp_exporter/blob/main/README.md#debugging")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a Go error, it shouldn't be this verbose.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the user see it on CLI?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but basically no. That's the whole reason for the "Possible old config file" error. The user won't directly see this error. It's for internal errors.Is() use.

)

// Config for the snmp_exporter.
type Config map[string]*Module
type Config struct {
Auths map[string]*Auth `yaml:"auths",omitempty"`
Modules map[string]*Module `yaml:"modules",omitempty"`
Version int `yaml:"version"`
}

type WalkParams struct {
Version int `yaml:"version,omitempty"`
MaxRepetitions uint32 `yaml:"max_repetitions,omitempty"`
Retries *int `yaml:"retries,omitempty"`
Timeout time.Duration `yaml:"timeout,omitempty"`
Auth Auth `yaml:"auth,omitempty"`
UseUnconnectedUDPSocket bool `yaml:"use_unconnected_udp_socket,omitempty"`
AllowNonIncreasingOIDs bool `yaml:"allow_nonincreasing_oids,omitempty"`
}
Expand All @@ -90,43 +97,11 @@ func (c *Module) UnmarshalYAML(unmarshal func(interface{}) error) error {
if err := unmarshal((*plain)(c)); err != nil {
return err
}

wp := c.WalkParams

if wp.Version < 1 || wp.Version > 3 {
return fmt.Errorf("SNMP version must be 1, 2 or 3. Got: %d", wp.Version)
}
if wp.Version == 3 {
switch wp.Auth.SecurityLevel {
case "authPriv":
if wp.Auth.PrivPassword == "" {
return fmt.Errorf("priv password is missing, required for SNMPv3 with priv")
}
if wp.Auth.PrivProtocol != "DES" && wp.Auth.PrivProtocol != "AES" && wp.Auth.PrivProtocol != "AES192" && wp.Auth.PrivProtocol != "AES192C" && wp.Auth.PrivProtocol != "AES256" && wp.Auth.PrivProtocol != "AES256C" {
return fmt.Errorf("priv protocol must be DES or AES")
}
fallthrough
case "authNoPriv":
if wp.Auth.Password == "" {
return fmt.Errorf("auth password is missing, required for SNMPv3 with auth")
}
if wp.Auth.AuthProtocol != "MD5" && wp.Auth.AuthProtocol != "SHA" && wp.Auth.AuthProtocol != "SHA224" && wp.Auth.AuthProtocol != "SHA256" && wp.Auth.AuthProtocol != "SHA384" && wp.Auth.AuthProtocol != "SHA512" {
return fmt.Errorf("auth protocol must be SHA or MD5")
}
fallthrough
case "noAuthNoPriv":
if wp.Auth.Username == "" {
return fmt.Errorf("auth username is missing, required for SNMPv3")
}
default:
return fmt.Errorf("security level must be one of authPriv, authNoPriv or noAuthNoPriv")
}
}
return nil
}

// ConfigureSNMP sets the various version and auth settings.
func (c WalkParams) ConfigureSNMP(g *gosnmp.GoSNMP) {
func (c Auth) ConfigureSNMP(g *gosnmp.GoSNMP) {
switch c.Version {
case 1:
g.Version = gosnmp.Version1
Expand All @@ -135,16 +110,16 @@ func (c WalkParams) ConfigureSNMP(g *gosnmp.GoSNMP) {
case 3:
g.Version = gosnmp.Version3
}
g.Community = string(c.Auth.Community)
g.ContextName = c.Auth.ContextName
g.Community = string(c.Community)
g.ContextName = c.ContextName

// v3 security settings.
g.SecurityModel = gosnmp.UserSecurityModel
usm := &gosnmp.UsmSecurityParameters{
UserName: c.Auth.Username,
UserName: c.Username,
}
auth, priv := false, false
switch c.Auth.SecurityLevel {
switch c.SecurityLevel {
case "noAuthNoPriv":
g.MsgFlags = gosnmp.NoAuthNoPriv
case "authNoPriv":
Expand All @@ -156,8 +131,8 @@ func (c WalkParams) ConfigureSNMP(g *gosnmp.GoSNMP) {
priv = true
}
if auth {
usm.AuthenticationPassphrase = string(c.Auth.Password)
switch c.Auth.AuthProtocol {
usm.AuthenticationPassphrase = string(c.Password)
switch c.AuthProtocol {
case "SHA":
usm.AuthenticationProtocol = gosnmp.SHA
case "SHA224":
Expand All @@ -173,8 +148,8 @@ func (c WalkParams) ConfigureSNMP(g *gosnmp.GoSNMP) {
}
}
if priv {
usm.PrivacyPassphrase = string(c.Auth.PrivPassword)
switch c.Auth.PrivProtocol {
usm.PrivacyPassphrase = string(c.PrivPassword)
switch c.PrivProtocol {
case "DES":
usm.PrivacyProtocol = gosnmp.DES
case "AES":
Expand Down Expand Up @@ -261,6 +236,46 @@ type Auth struct {
PrivProtocol string `yaml:"priv_protocol,omitempty"`
PrivPassword Secret `yaml:"priv_password,omitempty"`
ContextName string `yaml:"context_name,omitempty"`
Version int `yaml:"version,omitempty"`
}

func (c *Auth) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultAuth
type plain Auth
if err := unmarshal((*plain)(c)); err != nil {
return err
}

if c.Version < 1 || c.Version > 3 {
return fmt.Errorf("SNMP version must be 1, 2 or 3. Got: %d", c.Version)
}
if c.Version == 3 {
switch c.SecurityLevel {
case "authPriv":
if c.PrivPassword == "" {
return fmt.Errorf("priv password is missing, required for SNMPv3 with priv")
}
if c.PrivProtocol != "DES" && c.PrivProtocol != "AES" && c.PrivProtocol != "AES192" && c.PrivProtocol != "AES192C" && c.PrivProtocol != "AES256" && c.PrivProtocol != "AES256C" {
return fmt.Errorf("priv protocol must be DES or AES")
}
fallthrough
case "authNoPriv":
if c.Password == "" {
return fmt.Errorf("auth password is missing, required for SNMPv3 with auth")
}
if c.AuthProtocol != "MD5" && c.AuthProtocol != "SHA" && c.AuthProtocol != "SHA224" && c.AuthProtocol != "SHA256" && c.AuthProtocol != "SHA384" && c.AuthProtocol != "SHA512" {
return fmt.Errorf("auth protocol must be SHA or MD5")
}
fallthrough
case "noAuthNoPriv":
if c.Username == "" {
return fmt.Errorf("auth username is missing, required for SNMPv3")
}
default:
return fmt.Errorf("security level must be one of authPriv, authNoPriv or noAuthNoPriv")
}
}
return nil
}

type RegexpExtract struct {
Expand Down
Loading