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  }