diff --git a/collector/collector.go b/collector/collector.go index 0ffe7ece..3f136c66 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -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 } @@ -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() @@ -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 @@ -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() @@ -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. @@ -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) diff --git a/config/config.go b/config/config.go index f8fd56d1..31af8bd5 100644 --- a/config/config.go +++ b/config/config.go @@ -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, } @@ -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"` } @@ -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 @@ -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": @@ -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": @@ -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": @@ -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 { diff --git a/generator/config.go b/generator/config.go index 460808b1..13f23776 100644 --- a/generator/config.go +++ b/generator/config.go @@ -21,6 +21,7 @@ import ( // The generator config. type Config struct { + Auths map[string]*config.Auth `yaml:"auths"` Modules map[string]*ModuleConfig `yaml:"modules"` } diff --git a/generator/generator.yml b/generator/generator.yml index 7ce742fb..562a09a9 100644 --- a/generator/generator.yml +++ b/generator/generator.yml @@ -1,3 +1,9 @@ +auths: + public: + version: 2 + public_v1: + version: 1 + modules: # Default IF-MIB interfaces table with ifIndex. if_mib: @@ -60,7 +66,6 @@ modules: # Guide: http://www.apc.com/salestools/ASTE-6Z5QEY/ASTE-6Z5QEY_R0_EN.pdf # Download site: http://www.apc.com/us/en/tools/download/index.cfm apcups: - version: 1 walk: - sysUpTime - interfaces @@ -315,7 +320,6 @@ modules: # https://www.ui.com/downloads/firmwares/airfiber5X/v4.0.5/UBNT-MIB.txt # ubiquiti_airfiber: - version: 1 walk: - sysUpTime - interfaces @@ -330,7 +334,6 @@ modules: # https://dl.ubnt.com/firmwares/airos-ubnt-mib/ubnt-mib.zip # ubiquiti_airmax: - version: 1 walk: - sysUpTime - interfaces @@ -536,8 +539,6 @@ modules: overrides: outletOperationalState: type: EnumAsStateSet - auth: - community: raritan_public # Wiener Power Supply Module MPod # @@ -662,7 +663,6 @@ modules: # # https://www.cyberpowersystems.com/product/software/mib-files/mib-v29/ cyberpower: - version: 1 walk: - 1.3.6.1.4.1.3808.1.1.1 # ups - 1.3.6.1.4.1.3808.1.1.4 # environmentSensor diff --git a/generator/main.go b/generator/main.go index 113dde05..cad6c170 100644 --- a/generator/main.go +++ b/generator/main.go @@ -47,6 +47,8 @@ func generateConfig(nodes *Node, nameToNode map[string]*Node, logger log.Logger) } outputConfig := config.Config{} + outputConfig.Auths = cfg.Auths + outputConfig.Modules = make(map[string]*config.Module, len(cfg.Modules)) for name, m := range cfg.Modules { level.Info(logger).Log("msg", "Generating config for module", "module", name) // Give each module a copy of the tree so that it can be modified. @@ -61,9 +63,9 @@ func generateConfig(nodes *Node, nameToNode map[string]*Node, logger log.Logger) if err != nil { return err } - outputConfig[name] = out - outputConfig[name].WalkParams = m.WalkParams - level.Info(logger).Log("msg", "Generated metrics", "module", name, "metrics", len(outputConfig[name].Metrics)) + outputConfig.Modules[name] = out + outputConfig.Modules[name].WalkParams = m.WalkParams + level.Info(logger).Log("msg", "Generated metrics", "module", name, "metrics", len(outputConfig.Modules[name].Metrics)) } config.DoNotHideSecrets = true diff --git a/main.go b/main.go index c249ffd6..5a22ba89 100644 --- a/main.go +++ b/main.go @@ -51,7 +51,7 @@ var ( Name: "snmp_collection_duration_seconds", Help: "Duration of collections by the SNMP exporter", }, - []string{"module"}, + []string{"auth", "module"}, ) snmpRequestErrors = promauto.NewCounter( prometheus.CounterOpts{ @@ -60,7 +60,8 @@ var ( }, ) sc = &SafeConfig{ - C: &config.Config{}, + Auths: make(map[string]*config.Auth), + Modules: make(map[string]*config.Module), } reloadCh chan chan error ) @@ -75,6 +76,16 @@ func handler(w http.ResponseWriter, r *http.Request, logger log.Logger) { return } + authName := query.Get("auth") + if len(query["auth"]) > 1 { + http.Error(w, "'auth' parameter must only be specified once", 400) + snmpRequestErrors.Inc() + return + } + if authName == "" { + authName = "public" + } + moduleName := query.Get("module") if len(query["module"]) > 1 { http.Error(w, "'module' parameter must only be specified once", 400) @@ -84,27 +95,34 @@ func handler(w http.ResponseWriter, r *http.Request, logger log.Logger) { if moduleName == "" { moduleName = "if_mib" } + sc.RLock() - module, ok := (*(sc.C))[moduleName] + auth, authOk := sc.Auths[authName] + module, moduleOk := sc.Modules[moduleName] sc.RUnlock() - if !ok { + if !authOk { + http.Error(w, fmt.Sprintf("Unknown auth '%s'", authName), 400) + snmpRequestErrors.Inc() + return + } + if !moduleOk { http.Error(w, fmt.Sprintf("Unknown module '%s'", moduleName), 400) snmpRequestErrors.Inc() return } - logger = log.With(logger, "module", moduleName, "target", target) + logger = log.With(logger, "auth", authName, "module", moduleName, "target", target) level.Debug(logger).Log("msg", "Starting scrape") start := time.Now() registry := prometheus.NewRegistry() - c := collector.New(r.Context(), target, module, logger) + c := collector.New(r.Context(), target, auth, module, logger) registry.MustRegister(c) // Delegate http serving to Prometheus client library, which will call collector.Collect. h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) h.ServeHTTP(w, r) duration := time.Since(start).Seconds() - snmpDuration.WithLabelValues(moduleName).Observe(duration) + snmpDuration.WithLabelValues(authName, moduleName).Observe(duration) level.Debug(logger).Log("msg", "Finished scrape", "duration_seconds", duration) } @@ -123,7 +141,8 @@ func updateConfiguration(w http.ResponseWriter, r *http.Request) { type SafeConfig struct { sync.RWMutex - C *config.Config + Auths map[string]*config.Auth + Modules map[string]*config.Module } func (sc *SafeConfig) ReloadConfig(configFile string) (err error) { @@ -132,7 +151,8 @@ func (sc *SafeConfig) ReloadConfig(configFile string) (err error) { return err } sc.Lock() - sc.C = conf + sc.Auths = conf.Auths + sc.Modules = conf.Modules sc.Unlock() return nil } @@ -152,11 +172,15 @@ func main() { // Bail early if the config is bad. var err error - sc.C, err = config.LoadFile(*configFile) + newConfig, err := config.LoadFile(*configFile) if err != nil { level.Error(logger).Log("msg", "Error parsing config file", "err", err) os.Exit(1) } + sc.Lock() + sc.Auths = newConfig.Auths + sc.Modules = newConfig.Modules + sc.Unlock() // Exit if in dry-run mode. if *dryRun { @@ -164,10 +188,14 @@ func main() { return } + sc.RLock() // Initialize metrics. - for module := range *sc.C { - snmpDuration.WithLabelValues(module) + for auth := range sc.Auths { + for module := range sc.Modules { + snmpDuration.WithLabelValues(auth, module) + } } + sc.RUnlock() hup := make(chan os.Signal, 1) reloadCh = make(chan chan error) @@ -221,17 +249,18 @@ func main() {