diff --git a/conf/input.snmp/snmp.toml b/conf/input.snmp/snmp.toml index 7f151c5d..742fad48 100644 --- a/conf/input.snmp/snmp.toml +++ b/conf/input.snmp/snmp.toml @@ -29,6 +29,7 @@ agents = [ ## Used by the gosmi translator. ## To add paths when translating with netsnmp, use the MIBDIRS environment variable # path = ["/usr/share/snmp/mibs"] +# translator = "gosmi" ## SNMP community string. # community = "public" diff --git a/inputs/snmp/gosmi.go b/inputs/snmp/gosmi.go new file mode 100644 index 00000000..e43ab6f2 --- /dev/null +++ b/inputs/snmp/gosmi.go @@ -0,0 +1,141 @@ +package snmp + +import ( + "fmt" + "sync" + + "flashcat.cloud/categraf/pkg/snmp" + "github.com/sleepinggenius2/gosmi" + "github.com/sleepinggenius2/gosmi/models" +) + +type gosmiTranslator struct { +} + +func NewGosmiTranslator(paths []string) (*gosmiTranslator, error) { + err := snmp.LoadMibsFromPath(paths, &snmp.GosmiMibLoader{}) + if err == nil { + return &gosmiTranslator{}, nil + } + return nil, err +} + +type gosmiSnmpTranslateCache struct { + mibName string + oidNum string + oidText string + conversion string + node gosmi.SmiNode + err error +} + +var gosmiSnmpTranslateCachesLock sync.Mutex +var gosmiSnmpTranslateCaches map[string]gosmiSnmpTranslateCache + +//nolint:revive //function-result-limit conditionally 5 return results allowed +func (g *gosmiTranslator) SnmpTranslate(oid string) (mibName string, oidNum string, oidText string, conversion string, err error) { + mibName, oidNum, oidText, conversion, _, err = g.SnmpTranslateFull(oid) + return mibName, oidNum, oidText, conversion, err +} + +//nolint:revive //function-result-limit conditionally 6 return results allowed +func (g *gosmiTranslator) SnmpTranslateFull(oid string) ( + mibName string, oidNum string, oidText string, + conversion string, + node gosmi.SmiNode, + err error) { + gosmiSnmpTranslateCachesLock.Lock() + if gosmiSnmpTranslateCaches == nil { + gosmiSnmpTranslateCaches = map[string]gosmiSnmpTranslateCache{} + } + + var stc gosmiSnmpTranslateCache + var ok bool + if stc, ok = gosmiSnmpTranslateCaches[oid]; !ok { + // This will result in only one call to snmptranslate running at a time. + // We could speed it up by putting a lock in snmpTranslateCache and then + // returning it immediately, and multiple callers would then release the + // snmpTranslateCachesLock and instead wait on the individual + // snmpTranslation.Lock to release. But I don't know that the extra complexity + // is worth it. Especially when it would slam the system pretty hard if lots + // of lookups are being performed. + + stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.node, stc.err = snmp.SnmpTranslateCall(oid) + gosmiSnmpTranslateCaches[oid] = stc + } + + gosmiSnmpTranslateCachesLock.Unlock() + + return stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.node, stc.err +} + +type gosmiSnmpTableCache struct { + mibName string + oidNum string + oidText string + fields []Field + err error +} + +var gosmiSnmpTableCaches map[string]gosmiSnmpTableCache +var gosmiSnmpTableCachesLock sync.Mutex + +// snmpTable resolves the given OID as a table, providing information about the +// table and fields within. +// +//nolint:revive //Too many return variable but necessary +func (g *gosmiTranslator) SnmpTable(oid string) ( + mibName string, oidNum string, oidText string, + fields []Field, + err error) { + gosmiSnmpTableCachesLock.Lock() + if gosmiSnmpTableCaches == nil { + gosmiSnmpTableCaches = map[string]gosmiSnmpTableCache{} + } + + var stc gosmiSnmpTableCache + var ok bool + if stc, ok = gosmiSnmpTableCaches[oid]; !ok { + stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err = g.SnmpTableCall(oid) + gosmiSnmpTableCaches[oid] = stc + } + + gosmiSnmpTableCachesLock.Unlock() + return stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err +} + +//nolint:revive //Too many return variable but necessary +func (g *gosmiTranslator) SnmpTableCall(oid string) (mibName string, oidNum string, oidText string, fields []Field, err error) { + mibName, oidNum, oidText, _, node, err := g.SnmpTranslateFull(oid) + if err != nil { + return "", "", "", nil, fmt.Errorf("translating: %w", err) + } + + mibPrefix := mibName + "::" + + col, tagOids := snmp.GetIndex(mibPrefix, node) + for _, c := range col { + _, isTag := tagOids[mibPrefix+c] + fields = append(fields, Field{Name: c, Oid: mibPrefix + c, IsTag: isTag}) + } + + return mibName, oidNum, oidText, fields, nil +} + +func (g *gosmiTranslator) SnmpFormatEnum(oid string, value interface{}, full bool) (string, error) { + //nolint:dogsled // only need to get the node + _, _, _, _, node, err := g.SnmpTranslateFull(oid) + + if err != nil { + return "", err + } + + var v models.Value + if full { + v = node.FormatValue(value, models.FormatEnumName, models.FormatEnumValue) + } else { + v = node.FormatValue(value, models.FormatEnumName) + } + + return v.Formatted, nil +} diff --git a/inputs/snmp/instances.go b/inputs/snmp/instances.go index a6bc9adc..a2dbae7c 100644 --- a/inputs/snmp/instances.go +++ b/inputs/snmp/instances.go @@ -37,6 +37,8 @@ type Instance struct { connectionCache []snmpConnection + Translator string `toml:"translator"` + translator Translator Mappings map[string]map[string]string `toml:"mappings"` @@ -48,7 +50,13 @@ func (ins *Instance) Init() error { return types.ErrInstancesEmpty } + var err error switch ins.Translator { + case "gosmi": + ins.translator, err = NewGosmiTranslator(ins.Path) + if err != nil { + return err + } case "", "netsnmp": ins.translator = NewNetsnmpTranslator() default: diff --git a/inputs/snmp/netsnmp.go b/inputs/snmp/netsnmp.go index 031f82d9..691aa862 100644 --- a/inputs/snmp/netsnmp.go +++ b/inputs/snmp/netsnmp.go @@ -3,8 +3,9 @@ package snmp import ( "bufio" "bytes" + "errors" "fmt" - "log" //nolint:revive + "log" "os/exec" "strings" "sync" @@ -58,7 +59,6 @@ var snmpTableCachesLock sync.Mutex // snmpTable resolves the given OID as a table, providing information about the // table and fields within. -//nolint:revive func (n *netsnmpTranslator) SnmpTable(oid string) ( mibName string, oidNum string, oidText string, fields []Field, @@ -79,7 +79,6 @@ func (n *netsnmpTranslator) SnmpTable(oid string) ( return stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err } -//nolint:revive func (n *netsnmpTranslator) snmpTableCall(oid string) ( mibName string, oidNum string, oidText string, fields []Field, @@ -154,6 +153,7 @@ var snmpTranslateCachesLock sync.Mutex var snmpTranslateCaches map[string]snmpTranslateCache // snmpTranslate resolves the given OID. +// //nolint:revive func (n *netsnmpTranslator) SnmpTranslate(oid string) ( mibName string, oidNum string, oidText string, @@ -254,3 +254,7 @@ func snmpTranslateCall(oid string) (mibName string, oidNum string, oidText strin return mibName, oidNum, oidText, conversion, nil } + +func (n *netsnmpTranslator) SnmpFormatEnum(_ string, _ interface{}, _ bool) (string, error) { + return "", errors.New("not implemented in netsnmp translator") +} diff --git a/inputs/snmp/snmp.go b/inputs/snmp/snmp.go index 69655f52..b6d7689e 100644 --- a/inputs/snmp/snmp.go +++ b/inputs/snmp/snmp.go @@ -19,6 +19,11 @@ type Translator interface { fields []Field, err error, ) + + SnmpFormatEnum(oid string, value interface{}, full bool) ( + formatted string, + err error, + ) } type ClientConfig struct { diff --git a/pkg/snmp/config.go b/pkg/snmp/config.go deleted file mode 100644 index bddeef83..00000000 --- a/pkg/snmp/config.go +++ /dev/null @@ -1,39 +0,0 @@ -package snmp - -import ( - "time" -) - -type ClientConfig struct { - // Timeout to wait for a response. - Timeout time.Duration `toml:"timeout"` - Retries int `toml:"retries"` - // Values: 1, 2, 3 - Version uint8 `toml:"version"` - UnconnectedUDPSocket bool `toml:"unconnected_udp_socket"` - // Path to mib files - Path []string `toml:"path"` - // Translator implementation - Translator string `toml:"-"` - - // Parameters for Version 1 & 2 - Community string `toml:"community"` - - // Parameters for Version 2 & 3 - MaxRepetitions uint32 `toml:"max_repetitions"` - - // Parameters for Version 3 - ContextName string `toml:"context_name"` - // Values: "noAuthNoPriv", "authNoPriv", "authPriv" - SecLevel string `toml:"sec_level"` - SecName string `toml:"sec_name"` - // Values: "MD5", "SHA", "". Default: "" - AuthProtocol string `toml:"auth_protocol"` - AuthPassword string `toml:"auth_password"` - // Values: "DES", "AES", "". Default: "" - PrivProtocol string `toml:"priv_protocol"` - PrivPassword string `toml:"priv_password"` - EngineID string `toml:"-"` - EngineBoots uint32 `toml:"-"` - EngineTime uint32 `toml:"-"` -} diff --git a/pkg/snmp/wrapper.go b/pkg/snmp/wrapper.go deleted file mode 100644 index 88fa3d65..00000000 --- a/pkg/snmp/wrapper.go +++ /dev/null @@ -1,179 +0,0 @@ -package snmp - -import ( - "fmt" - "net/url" - "strconv" - "strings" - "time" - - "github.com/gosnmp/gosnmp" -) - -// GosnmpWrapper wraps a *gosnmp.GoSNMP object so we can use it as a snmpConnection. -type GosnmpWrapper struct { - *gosnmp.GoSNMP -} - -// Host returns the value of GoSNMP.Target. -func (gs GosnmpWrapper) Host() string { - return gs.Target -} - -// Walk wraps GoSNMP.Walk() or GoSNMP.BulkWalk(), depending on whether the -// connection is using SNMPv1 or newer. -func (gs GosnmpWrapper) Walk(oid string, fn gosnmp.WalkFunc) error { - if gs.Version == gosnmp.Version1 { - return gs.GoSNMP.Walk(oid, fn) - } - return gs.GoSNMP.BulkWalk(oid, fn) -} - -func NewWrapper(s ClientConfig) (GosnmpWrapper, error) { - gs := GosnmpWrapper{&gosnmp.GoSNMP{}} - - gs.Timeout = time.Duration(s.Timeout) - - gs.Retries = s.Retries - - gs.UseUnconnectedUDPSocket = s.UnconnectedUDPSocket - - switch s.Version { - case 3: - gs.Version = gosnmp.Version3 - case 2, 0: - gs.Version = gosnmp.Version2c - case 1: - gs.Version = gosnmp.Version1 - default: - return GosnmpWrapper{}, fmt.Errorf("invalid version") - } - - if s.Version < 3 { - if s.Community == "" { - gs.Community = "public" - } else { - gs.Community = s.Community - } - } - - gs.MaxRepetitions = s.MaxRepetitions - - if s.Version == 3 { - gs.ContextName = s.ContextName - - sp := &gosnmp.UsmSecurityParameters{} - gs.SecurityParameters = sp - gs.SecurityModel = gosnmp.UserSecurityModel - - switch strings.ToLower(s.SecLevel) { - case "noauthnopriv", "": - gs.MsgFlags = gosnmp.NoAuthNoPriv - case "authnopriv": - gs.MsgFlags = gosnmp.AuthNoPriv - case "authpriv": - gs.MsgFlags = gosnmp.AuthPriv - default: - return GosnmpWrapper{}, fmt.Errorf("invalid secLevel") - } - - sp.UserName = s.SecName - - switch strings.ToLower(s.AuthProtocol) { - case "md5": - sp.AuthenticationProtocol = gosnmp.MD5 - case "sha": - sp.AuthenticationProtocol = gosnmp.SHA - case "sha224": - sp.AuthenticationProtocol = gosnmp.SHA224 - case "sha256": - sp.AuthenticationProtocol = gosnmp.SHA256 - case "sha384": - sp.AuthenticationProtocol = gosnmp.SHA384 - case "sha512": - sp.AuthenticationProtocol = gosnmp.SHA512 - case "": - sp.AuthenticationProtocol = gosnmp.NoAuth - default: - return GosnmpWrapper{}, fmt.Errorf("invalid authProtocol") - } - - sp.AuthenticationPassphrase = s.AuthPassword - - switch strings.ToLower(s.PrivProtocol) { - case "des": - sp.PrivacyProtocol = gosnmp.DES - case "aes": - sp.PrivacyProtocol = gosnmp.AES - case "aes192": - sp.PrivacyProtocol = gosnmp.AES192 - case "aes192c": - sp.PrivacyProtocol = gosnmp.AES192C - case "aes256": - sp.PrivacyProtocol = gosnmp.AES256 - case "aes256c": - sp.PrivacyProtocol = gosnmp.AES256C - case "": - sp.PrivacyProtocol = gosnmp.NoPriv - default: - return GosnmpWrapper{}, fmt.Errorf("invalid privProtocol") - } - - sp.PrivacyPassphrase = s.PrivPassword - - sp.AuthoritativeEngineID = s.EngineID - - sp.AuthoritativeEngineBoots = s.EngineBoots - - sp.AuthoritativeEngineTime = s.EngineTime - } - return gs, nil -} - -// SetAgent takes a url (scheme://host:port) and sets the wrapped -// GoSNMP struct's corresponding fields. This shouldn't be called -// after using the wrapped GoSNMP struct, for example after -// connecting. -func (gs *GosnmpWrapper) SetAgent(agent string) error { - if !strings.Contains(agent, "://") { - agent = "udp://" + agent - } - - u, err := url.Parse(agent) - if err != nil { - return err - } - - // Only allow udp{4,6} and tcp{4,6}. - // Allowing ip{4,6} does not make sense as specifying a port - // requires the specification of a protocol. - // gosnmp does not handle these errors well, which is why - // they can result in cryptic errors by net.Dial. - switch u.Scheme { - case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6": - gs.Transport = u.Scheme - default: - return fmt.Errorf("unsupported scheme: %v", u.Scheme) - } - - gs.Target = u.Hostname() - - portStr := u.Port() - if portStr == "" { - portStr = "161" - } - port, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { - return fmt.Errorf("parsing port: %w", err) - } - gs.Port = uint16(port) - return nil -} - -func (gs GosnmpWrapper) Reconnect() error { - if gs.Conn == nil { - return gs.Connect() - } - - return nil -}