Files
akvorado/outlet/metadata/provider/snmp/poller.go
Vincent Bernat 0161c6ae31
Some checks failed
CI / 🤖 Check dependabot status (push) Has been cancelled
CI / 🐧 Test on Linux (${{ github.ref_type == 'tag' }}, misc) (push) Has been cancelled
CI / 🐧 Test on Linux (coverage) (push) Has been cancelled
CI / 🐧 Test on Linux (regular) (push) Has been cancelled
CI / ❄️ Build on Nix (push) Has been cancelled
CI / 🍏 Build and test on macOS (push) Has been cancelled
CI / 🧪 End-to-end testing (push) Has been cancelled
CI / 🔍 Upload code coverage (push) Has been cancelled
CI / 🔬 Test only Go (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 20) (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 22) (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 24) (push) Has been cancelled
CI / ⚖️ Check licenses (push) Has been cancelled
CI / 🐋 Build Docker images (push) Has been cancelled
CI / 🐋 Tag Docker images (push) Has been cancelled
CI / 🚀 Publish release (push) Has been cancelled
outlet/metadata: simplify error message for SNMP GET
Since we don't batch requests anymore, let's just tell which OIDs have a problem.
2025-12-04 16:01:04 +01:00

212 lines
6.5 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package snmp
import (
"context"
"errors"
"fmt"
"net/netip"
"slices"
"time"
"github.com/gosnmp/gosnmp"
"akvorado/outlet/metadata/provider"
)
// Poll polls the SNMP provider for the requested interface index.
func (p *Provider) Poll(ctx context.Context, exporter, agent netip.Addr, port uint16, ifIndex uint) (provider.Answer, error) {
exporterStr := exporter.Unmap().String()
// Instantiate an SNMP state
g := &gosnmp.GoSNMP{
Context: ctx,
Target: agent.Unmap().String(),
Port: port,
Retries: p.config.PollerRetries,
Timeout: p.config.PollerTimeout,
UseUnconnectedUDPSocket: true,
Logger: gosnmp.NewLogger(&goSNMPLogger{p.r}),
OnRetry: func(*gosnmp.GoSNMP) {
p.metrics.retries.WithLabelValues(exporterStr).Inc()
},
Version: gosnmp.Version2c,
}
communities := []string{"public"}
if credentials, ok := p.config.Credentials.Lookup(exporter); ok {
if credentials.UserName != "" {
g.Version = gosnmp.Version3
g.SecurityModel = gosnmp.UserSecurityModel
usmSecurityParameters := gosnmp.UsmSecurityParameters{
UserName: credentials.UserName,
AuthenticationProtocol: gosnmp.SnmpV3AuthProtocol(credentials.AuthenticationProtocol),
AuthenticationPassphrase: credentials.AuthenticationPassphrase,
PrivacyProtocol: gosnmp.SnmpV3PrivProtocol(credentials.PrivacyProtocol),
PrivacyPassphrase: credentials.PrivacyPassphrase,
}
g.SecurityParameters = &usmSecurityParameters
if usmSecurityParameters.AuthenticationProtocol == gosnmp.NoAuth {
if usmSecurityParameters.PrivacyProtocol == gosnmp.NoPriv {
g.MsgFlags = gosnmp.NoAuthNoPriv
} else {
// Not possible
g.MsgFlags = gosnmp.NoAuthNoPriv
}
} else {
if usmSecurityParameters.PrivacyProtocol == gosnmp.NoPriv {
g.MsgFlags = gosnmp.AuthNoPriv
} else {
g.MsgFlags = gosnmp.AuthPriv
}
}
g.ContextName = credentials.ContextName
} else {
g.Version = gosnmp.Version2c
communities = credentials.Communities
}
}
start := time.Now()
if err := g.Connect(); err != nil {
p.metrics.errors.WithLabelValues(exporterStr, "connect").Inc()
p.errLogger.Err(err).Str("exporter", exporterStr).Msg("unable to connect")
}
requests := []string{
"1.3.6.1.2.1.1.5.0",
fmt.Sprintf("1.3.6.1.2.1.2.2.1.2.%d", ifIndex), // ifDescr
fmt.Sprintf("1.3.6.1.2.1.31.1.1.1.1.%d", ifIndex), // ifName
fmt.Sprintf("1.3.6.1.2.1.31.1.1.1.18.%d", ifIndex), // ifAlias
fmt.Sprintf("1.3.6.1.2.1.31.1.1.1.15.%d", ifIndex), // ifSpeed
}
var results []gosnmp.SnmpPDU
success := false
logError := func(err error) error {
p.metrics.errors.WithLabelValues(exporterStr, "get").Inc()
p.errLogger.Err(err).
Str("exporter", exporterStr).
Msgf("unable to GET OIDs for index %d", ifIndex)
return err
}
for idx, community := range communities {
// Fatal error if last community and no success
isLast := idx == len(communities)-1
canError := isLast && !success
g.Community = community
currentResult, err := g.Get(requests)
if errors.Is(err, context.Canceled) {
return provider.Answer{}, err
}
if err != nil && canError {
return provider.Answer{}, logError(err)
}
if err != nil {
continue
}
if currentResult.Error != gosnmp.NoError && currentResult.ErrorIndex == 0 && canError {
// There is some error affecting the whole request
return provider.Answer{}, logError(fmt.Errorf("SNMP error %s(%d)", currentResult.Error, currentResult.Error))
}
success = true
if results == nil {
results = slices.Clone(currentResult.Variables)
} else {
if len(results) != len(currentResult.Variables) {
logError(errors.New("SNMP mismatch on variable lengths"))
}
for idx := range results {
switch results[idx].Type {
case gosnmp.NoSuchInstance, gosnmp.NoSuchObject, gosnmp.Null:
results[idx] = currentResult.Variables[idx]
}
}
}
}
if len(results) != len(requests) {
logError(errors.New("SNMP mismatch on variable lengths"))
}
p.metrics.times.WithLabelValues(exporterStr).Observe(time.Since(start).Seconds())
processStr := func(idx int, what string) (string, bool) {
switch results[idx].Type {
case gosnmp.OctetString:
return string(results[idx].Value.([]byte)), true
case gosnmp.NoSuchInstance, gosnmp.NoSuchObject, gosnmp.Null:
p.metrics.errors.WithLabelValues(exporterStr, fmt.Sprintf("%s missing", what)).Inc()
return "", false
default:
p.metrics.errors.WithLabelValues(exporterStr, fmt.Sprintf("%s unknown type", what)).Inc()
return "", false
}
}
processUint := func(idx int, what string) (uint, bool) {
switch results[idx].Type {
case gosnmp.Gauge32:
return results[idx].Value.(uint), true
case gosnmp.NoSuchInstance, gosnmp.NoSuchObject, gosnmp.Null:
p.metrics.errors.WithLabelValues(exporterStr, fmt.Sprintf("%s missing", what)).Inc()
return 0, false
default:
p.metrics.errors.WithLabelValues(exporterStr, fmt.Sprintf("%s unknown type", what)).Inc()
return 0, false
}
}
sysNameVal, ok := processStr(0, "sysname")
if !ok {
return provider.Answer{}, errors.New("unable to get sysName")
}
var (
name, description string
speed uint
)
ok = true
ifDescrVal, okDescr := processStr(1, "ifdescr")
ifNameVal, okName := processStr(2, "ifname")
ifAliasVal, okAlias := processStr(3, "ifalias")
ifSpeedVal, okSpeed := processUint(4, "ifspeed")
// Many equipments are using ifDescr for the interface name and
// ifAlias for the description, which is counter-intuitive. We want
// both the name and the description.
if okName {
// If we have ifName, use ifDescr if it is different and ifAlias
// is not. Otherwise, keep description empty.
name = ifNameVal
if okAlias && ifAliasVal != ifNameVal {
description = ifAliasVal
} else if okDescr && ifDescrVal != ifNameVal {
description = ifDescrVal
}
} else {
// Don't handle the other case yet. It would be unexpected to
// have ifAlias and not ifName. And if we have only ifDescr, we
// can't really know what this is.
ok = false
}
// Speed is mandatory
ok = ok && okSpeed
speed = ifSpeedVal
if ok {
p.metrics.successes.WithLabelValues(exporterStr).Inc()
return provider.Answer{
Found: true,
Exporter: provider.Exporter{
Name: sysNameVal,
},
Interface: provider.Interface{
Name: name,
Description: description,
Speed: speed,
},
}, nil
}
return provider.Answer{}, nil
}