diff --git a/README.md b/README.md index 4fed1192..2875cc05 100644 --- a/README.md +++ b/README.md @@ -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 @@ -68,9 +72,9 @@ 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?auth=public_v2&module=if_mib&target=192.0.0.8] where `192.0.0.8` is the IP or +FQDN of the SNMP device to get metrics from. Also note the use of default auth (`public_v2`) and +default module (`if_mib`). The auth and module must be defined in the `snmp.yml`. ## Configuration @@ -83,7 +87,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 @@ -95,6 +99,7 @@ scrape_configs: - switch.local # SNMP device. metrics_path: /snmp params: + auth: [public_v2] module: [if_mib] relabel_configs: - source_labels: [__address__] diff --git a/collector/collector.go b/collector/collector.go index 83438807..dc02bcfc 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -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 } @@ -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. err := snmp.Connect() @@ -172,9 +172,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{} @@ -204,7 +204,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 @@ -365,12 +365,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. @@ -381,7 +382,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 8eb20912..26462449 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"` } @@ -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 @@ -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": @@ -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": @@ -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": @@ -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 { diff --git a/generator/FORMAT.md b/generator/FORMAT.md index 0e97a6fa..80f4db0e 100644 --- a/generator/FORMAT.md +++ b/generator/FORMAT.md @@ -4,62 +4,65 @@ This is generated by the generator, so only those doing development should have to care about how this works. ``` -module_name: - auth: +auths: + public_v2: + version: 2 # 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 ``` diff --git a/generator/README.md b/generator/README.md index c267cf12..1c1e420b 100644 --- a/generator/README.md +++ b/generator/README.md @@ -49,10 +49,36 @@ make docker-generate ## File Format -`generator.yml` provides a list of modules. Each module defines what to collect from a device type. The simplest module is just a name -and a set of OIDs to walk. +`generator.yml` provides a list of auths and modules. Each module defines what to collect from a device type. +The simplest module is just a name and a set of OIDs to walk. ```yaml +auths: + auth_name: + version: 2 # SNMP version to use. Defaults to 2. + # 1 will use GETNEXT, 2 and 3 use GETBULK. + + # Community string is used with SNMP v1 and v2. Defaults to "public_v2". + community: public_v2 + + # v3 has different and more complex settings. + # Which are required depends on the security_level. + # The equivalent options on NetSNMP commands like snmpbulkwalk + # and snmpget are also listed. See snmpcmd(1). + username: user # Required, no default. -u option to NetSNMP. + security_level: noAuthNoPriv # Defaults to noAuthNoPriv. -l option to NetSNMP. + # Can be noAuthNoPriv, authNoPriv or authPriv. + password: pass # Has no default. Also known as authKey, -A option to NetSNMP. + # Required if security_level is authNoPriv or authPriv. + auth_protocol: MD5 # MD5, SHA, SHA224, SHA256, SHA384, or SHA512. Defaults to MD5. -a option to NetSNMP. + # Used if security_level is authNoPriv or authPriv. + priv_protocol: DES # DES, AES, AES192, or AES256. Defaults to DES. -x option to NetSNMP. + # Used if security_level is authPriv. + priv_password: otherPass # Has no default. Also known as privKey, -X option to NetSNMP. + # Required if security_level is authPriv. + context_name: context # Has no default. -n option to NetSNMP. + # Required if context is configured on the device. + modules: module_name: # The module name. You can have as many modules as you want. walk: # List of OIDs to walk. Can also be SNMP object names or specific instances. @@ -62,34 +88,11 @@ modules: - 1.3.6.1.2.1.2.2.1.4 # Same as ifMtu (used for filter example) - bsnDot11EssSsid # Same as 1.3.6.1.4.1.14179.2.1.1.1.2 (used for filter example) - version: 2 # SNMP version to use. Defaults to 2. - # 1 will use GETNEXT, 2 and 3 use GETBULK. max_repetitions: 25 # How many objects to request with GET/GETBULK, defaults to 25. # May need to be reduced for buggy devices. retries: 3 # How many times to retry a failed request, defaults to 3. timeout: 5s # Timeout for each individual SNMP request, defaults to 5s. - auth: - # Community string is used with SNMP v1 and v2. Defaults to "public". - community: public - - # v3 has different and more complex settings. - # Which are required depends on the security_level. - # The equivalent options on NetSNMP commands like snmpbulkwalk - # and snmpget are also listed. See snmpcmd(1). - username: user # Required, no default. -u option to NetSNMP. - security_level: noAuthNoPriv # Defaults to noAuthNoPriv. -l option to NetSNMP. - # Can be noAuthNoPriv, authNoPriv or authPriv. - password: pass # Has no default. Also known as authKey, -A option to NetSNMP. - # Required if security_level is authNoPriv or authPriv. - auth_protocol: MD5 # MD5, SHA, SHA224, SHA256, SHA384, or SHA512. Defaults to MD5. -a option to NetSNMP. - # Used if security_level is authNoPriv or authPriv. - priv_protocol: DES # DES, AES, AES192, or AES256. Defaults to DES. -x option to NetSNMP. - # Used if security_level is authPriv. - priv_password: otherPass # Has no default. Also known as privKey, -X option to NetSNMP. - # Required if security_level is authPriv. - context_name: context # Has no default. -n option to NetSNMP. - # Required if context is configured on the device. lookups: # Optional list of lookups to perform. # The default for `keep_source_indexes` is false. Indexes must be unique for this option to be used. @@ -135,7 +138,7 @@ modules: # PhysAddress48: A 48 bit MAC address, rendered as 00:01:02:03:04:ff. # Float: A 32 bit floating-point value with type gauge. # Double: A 64 bit floating-point value with type gauge. - # InetAddressIPv4: An IPv4 address, rendered as 1.2.3.4. + # InetAddressIPv4: An IPv4 address, rendered as 192.0.0.8. # InetAddressIPv6: An IPv6 address, rendered as 0102:0304:0506:0708:090A:0B0C:0D0E:0F10. # InetAddress: An InetAddress per RFC 4001. Must be preceded by an InetAddressType. # InetAddressMissingSize: An InetAddress that violates section 4.1 of RFC 4001 by diff --git a/generator/config.go b/generator/config.go index dcf81493..78a28c73 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..69c02c8f 100644 --- a/generator/generator.yml +++ b/generator/generator.yml @@ -1,3 +1,9 @@ +auths: + public_v1: + version: 1 + public_v2: + version: 2 + 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..2254fd4e 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{ @@ -70,41 +70,58 @@ func handler(w http.ResponseWriter, r *http.Request, logger log.Logger) { target := query.Get("target") if len(query["target"]) != 1 || target == "" { - http.Error(w, "'target' parameter must be specified once", 400) + http.Error(w, "'target' parameter must be specified once", http.StatusBadRequest) snmpRequestErrors.Inc() return } + authName := query.Get("auth") + if len(query["auth"]) > 1 { + http.Error(w, "'auth' parameter must only be specified once", http.StatusBadRequest) + snmpRequestErrors.Inc() + return + } + if authName == "" { + authName = "public_v2" + } + moduleName := query.Get("module") if len(query["module"]) > 1 { - http.Error(w, "'module' parameter must only be specified once", 400) + http.Error(w, "'module' parameter must only be specified once", http.StatusBadRequest) snmpRequestErrors.Inc() return } if moduleName == "" { moduleName = "if_mib" } + sc.RLock() - module, ok := (*(sc.C))[moduleName] + auth, authOk := sc.C.Auths[authName] + module, moduleOk := sc.C.Modules[moduleName] sc.RUnlock() - if !ok { - http.Error(w, fmt.Sprintf("Unknown module '%s'", moduleName), 400) + if !authOk { + http.Error(w, fmt.Sprintf("Unknown auth '%s'", authName), http.StatusBadRequest) + snmpRequestErrors.Inc() + return + } + if !moduleOk { + http.Error(w, fmt.Sprintf("Unknown module '%s'", moduleName), http.StatusBadRequest) 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) } @@ -117,7 +134,7 @@ func updateConfiguration(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError) } default: - http.Error(w, "POST method expected", 400) + http.Error(w, "POST method expected", http.StatusBadRequest) } } @@ -133,6 +150,12 @@ func (sc *SafeConfig) ReloadConfig(configFile string) (err error) { } sc.Lock() sc.C = conf + // Initialize metrics. + for auth := range sc.C.Auths { + for module := range sc.C.Modules { + snmpDuration.WithLabelValues(auth, module) + } + } sc.Unlock() return nil } @@ -151,8 +174,7 @@ func main() { prometheus.MustRegister(version.NewCollector("snmp_exporter")) // Bail early if the config is bad. - var err error - sc.C, err = config.LoadFile(*configFile) + err := sc.ReloadConfig(*configFile) if err != nil { level.Error(logger).Log("msg", "Error parsing config file", "err", err) os.Exit(1) @@ -164,11 +186,6 @@ func main() { return } - // Initialize metrics. - for module := range *sc.C { - snmpDuration.WithLabelValues(module) - } - hup := make(chan os.Signal, 1) reloadCh = make(chan chan error) signal.Notify(hup, syscall.SIGHUP) @@ -220,11 +237,12 @@ func main() {