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.

Fixes: #619

Signed-off-by: SuperQ <[email protected]>
  • Loading branch information
SuperQ committed Mar 26, 2023
1 parent 87bf22c commit 503e1f4
Show file tree
Hide file tree
Showing 12 changed files with 57,515 additions and 57,369 deletions.
27 changes: 14 additions & 13 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,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 @@ -158,7 +158,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.
err := snmp.Connect()
Expand All @@ -170,8 +170,8 @@ func ScrapeTarget(ctx context.Context, target string, config *config.Module, log
}
defer snmp.Conn.Close()

getOids := config.Get
maxOids := int(config.WalkParams.MaxRepetitions)
getOids := module.Get
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 @@ -213,7 +213,7 @@ func ScrapeTarget(ctx context.Context, target string, config *config.Module, log
getOids = getOids[oids:]
}

for _, subtree := range config.Walk {
for _, subtree := range module.Walk {
var pdus []gosnmp.SnmpPDU
level.Debug(logger).Log("msg", "Walking subtree", "oid", subtree)
walkStart := time.Now()
Expand Down Expand Up @@ -261,12 +261,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 @@ -277,7 +278,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 @@ -89,43 +89,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 @@ -134,16 +102,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 @@ -155,8 +123,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 @@ -172,8 +140,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 @@ -245,6 +213,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
4 changes: 2 additions & 2 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestHideConfigSecrets(t *testing.T) {

// String method must not reveal authentication credentials.
sc.RLock()
c, err := yaml.Marshal(sc.C)
c, err := yaml.Marshal(sc.Auths)
sc.RUnlock()
if err != nil {
t.Errorf("Error marshaling config: %v", err)
Expand All @@ -46,7 +46,7 @@ func TestLoadConfigWithOverrides(t *testing.T) {
t.Errorf("Error loading config %v: %v", "testdata/snmp-with-overrides.yml", err)
}
sc.RLock()
_, err = yaml.Marshal(sc.C)
_, err = yaml.Marshal(sc.Modules)
sc.RUnlock()
if err != nil {
t.Errorf("Error marshaling config: %v", err)
Expand Down
114 changes: 58 additions & 56 deletions generator/FORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,64 @@ This is generated by the generator, so only those doing development should
have to care about how this works.

```
module_name:
auth:
auths:
public:
# There's various auth/version options here too. See the main README.
community: public
walk:
# List of OID subtrees to walk.
- 1.3.6.1.2.1.2
- 1.3.6.1.2.1.31.1.1
get:
# List of OIDs to get directly.
- 1.3.6.1.2.1.1.3
metrics: # List of metrics to extract.
# A simple metric with no labels.
- name: sysUpTime
oid: 1.3.6.1.2.1.1.3
type: gauge
# See README.md type override for a list of valid types
# Non-numeric types are represented as a gauge with value 1, and the rendered value
# as a label value on that gauge.
# A metric that's part of a table, and thus has labels.
- name: ifMtu
oid: 1.3.6.1.2.1.2.2.1.4
type: gauge
# A list of the table indexes and their types. All indexes become labels.
indexes:
- labelname: ifIndex
type: gauge
- labelname: someString
type: OctetString
fixed_size: 8 # Only possible for OctetString/DisplayString types.
# If only one length is possible this is it. Otherwise
# this will be 0 or missing.
- labelname: someOtherString
type: OctetString
implied: true # Only possible for OctetString/DisplayString types.
# Must be the last index. See RFC2578 section 7.7.
- name: ifSpeed
oid: 1.3.6.1.2.1.2.2.1.5
type: gauge
indexes:
- labelname: ifDescr
type: gauge
# Lookups take original indexes, look them up in another part of the
# oid tree and overwrite the given output label.
lookups:
- labels: [ifDescr] # Input label name(s). Empty means delete the output label.
oid: 1.3.6.1.2.1.2.2.1.2 # OID to look under.
labelname: ifDescr # Output label name.
type: OctetString # Type of output object.
# Creates new metrics based on the regex and the metric value.
regex_extracts:
Temp: # A new metric will be created appending this to the metricName to become metricNameTemp.
- regex: '(.*)' # Regex to extract a value from the returned SNMP walks's value.
value: '$1' # Parsed as float64, defaults to $1.
enum_values: # Enum for this metric. Only used with the enum types.
0: true
1: false
modules:
module_name:
walk:
# List of OID subtrees to walk.
- 1.3.6.1.2.1.2
- 1.3.6.1.2.1.31.1.1
get:
# List of OIDs to get directly.
- 1.3.6.1.2.1.1.3
metrics: # List of metrics to extract.
# A simple metric with no labels.
- name: sysUpTime
oid: 1.3.6.1.2.1.1.3
type: gauge
# See README.md type override for a list of valid types
# Non-numeric types are represented as a gauge with value 1, and the rendered value
# as a label value on that gauge.
# A metric that's part of a table, and thus has labels.
- name: ifMtu
oid: 1.3.6.1.2.1.2.2.1.4
type: gauge
# A list of the table indexes and their types. All indexes become labels.
indexes:
- labelname: ifIndex
type: gauge
- labelname: someString
type: OctetString
fixed_size: 8 # Only possible for OctetString/DisplayString types.
# If only one length is possible this is it. Otherwise
# this will be 0 or missing.
- labelname: someOtherString
type: OctetString
implied: true # Only possible for OctetString/DisplayString types.
# Must be the last index. See RFC2578 section 7.7.
- name: ifSpeed
oid: 1.3.6.1.2.1.2.2.1.5
type: gauge
indexes:
- labelname: ifDescr
type: gauge
# Lookups take original indexes, look them up in another part of the
# oid tree and overwrite the given output label.
lookups:
- labels: [ifDescr] # Input label name(s). Empty means delete the output label.
oid: 1.3.6.1.2.1.2.2.1.2 # OID to look under.
labelname: ifDescr # Output label name.
type: OctetString # Type of output object.
# Creates new metrics based on the regex and the metric value.
regex_extracts:
Temp: # A new metric will be created appending this to the metricName to become metricNameTemp.
- regex: '(.*)' # Regex to extract a value from the returned SNMP walks's value.
value: '$1' # Parsed as float64, defaults to $1.
enum_values: # Enum for this metric. Only used with the enum types.
0: true
1: false
```
Loading

0 comments on commit 503e1f4

Please sign in to comment.