Skip to content

Commit

Permalink
Split config of auth and modules
Browse files Browse the repository at this point in the history
Allow configuration of auth/version parameters separately from the walk
and metrics in the generator and exporter configuration.
* Simplify startup with `ReloadConfig()`
* Make sure to init metrics on config reload.

Fixes: #619

Signed-off-by: SuperQ <[email protected]>
  • Loading branch information
SuperQ committed Jun 19, 2023
1 parent 3657f3e commit 5e99716
Show file tree
Hide file tree
Showing 14 changed files with 49,326 additions and 49,135 deletions.
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

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.

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
100 changes: 54 additions & 46 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,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 @@ -63,14 +62,15 @@ var (
)

// 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"`
}

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 +90,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 +103,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 +124,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 +141,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 +229,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

0 comments on commit 5e99716

Please sign in to comment.