github.com/influxdata/telegraf@v1.30.3/internal/snmp/translator_netsnmp.go (about) 1 package snmp 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "fmt" 8 "os/exec" 9 "strings" 10 "sync" 11 12 "github.com/influxdata/telegraf" 13 ) 14 15 // struct that implements the translator interface. This calls existing 16 // code to exec netsnmp's snmptranslate program 17 type netsnmpTranslator struct { 18 log telegraf.Logger 19 } 20 21 func NewNetsnmpTranslator(log telegraf.Logger) *netsnmpTranslator { 22 return &netsnmpTranslator{log: log} 23 } 24 25 type snmpTableCache struct { 26 mibName string 27 oidNum string 28 oidText string 29 fields []Field 30 err error 31 } 32 33 // execCommand is so tests can mock out exec.Command usage. 34 var execCommand = exec.Command 35 36 // execCmd executes the specified command, returning the STDOUT content. 37 // If command exits with error status, the output is captured into the returned error. 38 func (n *netsnmpTranslator) execCmd(arg0 string, args ...string) ([]byte, error) { 39 quoted := make([]string, 0, len(args)) 40 for _, arg := range args { 41 quoted = append(quoted, fmt.Sprintf("%q", arg)) 42 } 43 n.log.Debugf("executing %q %s", arg0, strings.Join(quoted, " ")) 44 45 out, err := execCommand(arg0, args...).Output() 46 if err != nil { 47 var exitErr *exec.ExitError 48 if errors.As(err, &exitErr) { 49 return nil, fmt.Errorf("%s: %w", bytes.TrimRight(exitErr.Stderr, "\r\n"), err) 50 } 51 return nil, err 52 } 53 return out, nil 54 } 55 56 var snmpTableCaches map[string]snmpTableCache 57 var snmpTableCachesLock sync.Mutex 58 59 // snmpTable resolves the given OID as a table, providing information about the 60 // table and fields within. 61 // 62 //nolint:revive //function-result-limit conditionally 5 return results allowed 63 func (n *netsnmpTranslator) SnmpTable(oid string) ( 64 mibName string, oidNum string, oidText string, 65 fields []Field, 66 err error) { 67 snmpTableCachesLock.Lock() 68 if snmpTableCaches == nil { 69 snmpTableCaches = map[string]snmpTableCache{} 70 } 71 72 var stc snmpTableCache 73 var ok bool 74 if stc, ok = snmpTableCaches[oid]; !ok { 75 stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err = n.snmpTableCall(oid) 76 snmpTableCaches[oid] = stc 77 } 78 79 snmpTableCachesLock.Unlock() 80 return stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err 81 } 82 83 //nolint:revive //function-result-limit conditionally 5 return results allowed 84 func (n *netsnmpTranslator) snmpTableCall(oid string) ( 85 mibName string, oidNum string, oidText string, 86 fields []Field, 87 err error) { 88 mibName, oidNum, oidText, _, err = n.SnmpTranslate(oid) 89 if err != nil { 90 return "", "", "", nil, fmt.Errorf("translating: %w", err) 91 } 92 93 mibPrefix := mibName + "::" 94 oidFullName := mibPrefix + oidText 95 96 // first attempt to get the table's tags 97 tagOids := map[string]struct{}{} 98 // We have to guess that the "entry" oid is `oid+".1"`. snmptable and snmptranslate don't seem to have a way to provide the info. 99 if out, err := n.execCmd("snmptranslate", "-Td", oidFullName+".1"); err == nil { 100 scanner := bufio.NewScanner(bytes.NewBuffer(out)) 101 for scanner.Scan() { 102 line := scanner.Text() 103 104 if !strings.HasPrefix(line, " INDEX") { 105 continue 106 } 107 108 i := strings.Index(line, "{ ") 109 if i == -1 { // parse error 110 continue 111 } 112 line = line[i+2:] 113 i = strings.Index(line, " }") 114 if i == -1 { // parse error 115 continue 116 } 117 line = line[:i] 118 for _, col := range strings.Split(line, ", ") { 119 tagOids[mibPrefix+col] = struct{}{} 120 } 121 } 122 } 123 124 // this won't actually try to run a query. The `-Ch` will just cause it to dump headers. 125 out, err := n.execCmd("snmptable", "-Ch", "-Cl", "-c", "public", "127.0.0.1", oidFullName) 126 if err != nil { 127 return "", "", "", nil, fmt.Errorf("getting table columns: %w", err) 128 } 129 scanner := bufio.NewScanner(bytes.NewBuffer(out)) 130 scanner.Scan() 131 cols := scanner.Text() 132 if len(cols) == 0 { 133 return "", "", "", nil, errors.New("could not find any columns in table") 134 } 135 for _, col := range strings.Split(cols, " ") { 136 if len(col) == 0 { 137 continue 138 } 139 _, isTag := tagOids[mibPrefix+col] 140 fields = append(fields, Field{Name: col, Oid: mibPrefix + col, IsTag: isTag}) 141 } 142 143 return mibName, oidNum, oidText, fields, err 144 } 145 146 type snmpTranslateCache struct { 147 mibName string 148 oidNum string 149 oidText string 150 conversion string 151 err error 152 } 153 154 var snmpTranslateCachesLock sync.Mutex 155 var snmpTranslateCaches map[string]snmpTranslateCache 156 157 // snmpTranslate resolves the given OID. 158 // 159 //nolint:revive //function-result-limit conditionally 5 return results allowed 160 func (n *netsnmpTranslator) SnmpTranslate(oid string) ( 161 mibName string, oidNum string, oidText string, 162 conversion string, 163 err error) { 164 snmpTranslateCachesLock.Lock() 165 if snmpTranslateCaches == nil { 166 snmpTranslateCaches = map[string]snmpTranslateCache{} 167 } 168 169 var stc snmpTranslateCache 170 var ok bool 171 if stc, ok = snmpTranslateCaches[oid]; !ok { 172 // This will result in only one call to snmptranslate running at a time. 173 // We could speed it up by putting a lock in snmpTranslateCache and then 174 // returning it immediately, and multiple callers would then release the 175 // snmpTranslateCachesLock and instead wait on the individual 176 // snmpTranslation.Lock to release. But I don't know that the extra complexity 177 // is worth it. Especially when it would slam the system pretty hard if lots 178 // of lookups are being performed. 179 180 stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.err = n.snmpTranslateCall(oid) 181 snmpTranslateCaches[oid] = stc 182 } 183 184 snmpTranslateCachesLock.Unlock() 185 186 return stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.err 187 } 188 189 //nolint:revive //function-result-limit conditionally 5 return results allowed 190 func (n *netsnmpTranslator) snmpTranslateCall(oid string) (mibName string, oidNum string, oidText string, conversion string, err error) { 191 var out []byte 192 if strings.ContainsAny(oid, ":abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") { 193 out, err = n.execCmd("snmptranslate", "-Td", "-Ob", oid) 194 } else { 195 out, err = n.execCmd("snmptranslate", "-Td", "-Ob", "-m", "all", oid) 196 var execErr *exec.Error 197 if errors.As(err, &execErr) && errors.Is(execErr, exec.ErrNotFound) { 198 // Silently discard error if snmptranslate not found and we have a numeric OID. 199 // Meaning we can get by without the lookup. 200 return "", oid, oid, "", nil 201 } 202 } 203 if err != nil { 204 return "", "", "", "", err 205 } 206 207 scanner := bufio.NewScanner(bytes.NewBuffer(out)) 208 ok := scanner.Scan() 209 if !ok && scanner.Err() != nil { 210 return "", "", "", "", fmt.Errorf("getting OID text: %w", scanner.Err()) 211 } 212 213 oidText = scanner.Text() 214 215 i := strings.Index(oidText, "::") 216 if i == -1 { 217 // was not found in MIB. 218 if bytes.Contains(out, []byte("[TRUNCATED]")) { 219 return "", oid, oid, "", nil 220 } 221 // not truncated, but not fully found. We still need to parse out numeric OID, so keep going 222 oidText = oid 223 } else { 224 mibName = oidText[:i] 225 oidText = oidText[i+2:] 226 } 227 228 for scanner.Scan() { 229 line := scanner.Text() 230 231 if strings.HasPrefix(line, " -- TEXTUAL CONVENTION ") { 232 tc := strings.TrimPrefix(line, " -- TEXTUAL CONVENTION ") 233 switch tc { 234 case "MacAddress", "PhysAddress": 235 conversion = "hwaddr" 236 case "InetAddressIPv4", "InetAddressIPv6", "InetAddress", "IPSIpAddress": 237 conversion = "ipaddr" 238 } 239 } else if strings.HasPrefix(line, "::= { ") { 240 objs := strings.TrimPrefix(line, "::= { ") 241 objs = strings.TrimSuffix(objs, " }") 242 243 for _, obj := range strings.Split(objs, " ") { 244 if len(obj) == 0 { 245 continue 246 } 247 if i := strings.Index(obj, "("); i != -1 { 248 obj = obj[i+1:] 249 if j := strings.Index(obj, ")"); j != -1 { 250 oidNum += "." + obj[:j] 251 } else { 252 return "", "", "", "", fmt.Errorf("getting OID number from: %s", obj) 253 } 254 255 } else { 256 oidNum += "." + obj 257 } 258 } 259 break 260 } 261 } 262 263 return mibName, oidNum, oidText, conversion, nil 264 } 265 266 func (n *netsnmpTranslator) SnmpFormatEnum(_ string, _ interface{}, _ bool) (string, error) { 267 return "", errors.New("not implemented in netsnmp translator") 268 }