github.com/influxdata/telegraf@v1.30.3/internal/snmp/table.go (about)

     1  package snmp
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/gosnmp/gosnmp"
    10  )
    11  
    12  // Table holds the configuration for a SNMP table.
    13  type Table struct {
    14  	// Name will be the name of the measurement.
    15  	Name string
    16  
    17  	// Which tags to inherit from the top-level config.
    18  	InheritTags []string
    19  
    20  	// Adds each row's table index as a tag.
    21  	IndexAsTag bool
    22  
    23  	// Fields is the tags and values to look up.
    24  	Fields []Field `toml:"field"`
    25  
    26  	// OID for automatic field population.
    27  	// If provided, init() will populate Fields with all the table columns of the
    28  	// given OID.
    29  	Oid string
    30  
    31  	initialized bool
    32  	translator  Translator
    33  }
    34  
    35  // RTable is the resulting table built from a Table.
    36  type RTable struct {
    37  	// Name is the name of the field, copied from Table.Name.
    38  	Name string
    39  	// Time is the time the table was built.
    40  	Time time.Time
    41  	// Rows are the rows that were found, one row for each table OID index found.
    42  	Rows []RTableRow
    43  }
    44  
    45  // RTableRow is the resulting row containing all the OID values which shared
    46  // the same index.
    47  type RTableRow struct {
    48  	// Tags are all the Field values which had IsTag=true.
    49  	Tags map[string]string
    50  	// Fields are all the Field values which had IsTag=false.
    51  	Fields map[string]interface{}
    52  }
    53  
    54  // Init() builds & initializes the nested fields.
    55  func (t *Table) Init(tr Translator) error {
    56  	//makes sure oid or name is set in config file
    57  	//otherwise snmp will produce metrics with an empty name
    58  	if t.Oid == "" && t.Name == "" {
    59  		return errors.New("SNMP table in config file is not named. One or both of the oid and name settings must be set")
    60  	}
    61  
    62  	if t.initialized {
    63  		return nil
    64  	}
    65  
    66  	t.translator = tr
    67  	if err := t.initBuild(); err != nil {
    68  		return err
    69  	}
    70  
    71  	secondaryIndexTablePresent := false
    72  	// initialize all the nested fields
    73  	for i := range t.Fields {
    74  		if err := t.Fields[i].Init(t.translator); err != nil {
    75  			return fmt.Errorf("initializing field %s: %w", t.Fields[i].Name, err)
    76  		}
    77  		if t.Fields[i].SecondaryIndexTable {
    78  			if secondaryIndexTablePresent {
    79  				return errors.New("only one field can be SecondaryIndexTable")
    80  			}
    81  			secondaryIndexTablePresent = true
    82  		}
    83  	}
    84  
    85  	t.initialized = true
    86  	return nil
    87  }
    88  
    89  // initBuild initializes the table if it has an OID configured. If so, the
    90  // net-snmp tools will be used to look up the OID and auto-populate the table's
    91  // fields.
    92  func (t *Table) initBuild() error {
    93  	if t.Oid == "" {
    94  		return nil
    95  	}
    96  
    97  	_, _, oidText, fields, err := t.translator.SnmpTable(t.Oid)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	if t.Name == "" {
   103  		t.Name = oidText
   104  	}
   105  
   106  	knownOIDs := map[string]bool{}
   107  	for _, f := range t.Fields {
   108  		knownOIDs[f.Oid] = true
   109  	}
   110  	for _, f := range fields {
   111  		if !knownOIDs[f.Oid] {
   112  			t.Fields = append(t.Fields, f)
   113  		}
   114  	}
   115  
   116  	return nil
   117  }
   118  
   119  // Build retrieves all the fields specified in the table and constructs the RTable.
   120  func (t Table) Build(gs Connection, walk bool) (*RTable, error) {
   121  	rows := map[string]RTableRow{}
   122  
   123  	//translation table for secondary index (when performing join on two tables)
   124  	secIdxTab := make(map[string]string)
   125  	secGlobalOuterJoin := false
   126  	for i, f := range t.Fields {
   127  		if f.SecondaryIndexTable {
   128  			secGlobalOuterJoin = f.SecondaryOuterJoin
   129  			if i != 0 {
   130  				t.Fields[0], t.Fields[i] = t.Fields[i], t.Fields[0]
   131  			}
   132  			break
   133  		}
   134  	}
   135  
   136  	tagCount := 0
   137  	for _, f := range t.Fields {
   138  		if f.IsTag {
   139  			tagCount++
   140  		}
   141  
   142  		if len(f.Oid) == 0 {
   143  			return nil, fmt.Errorf("cannot have empty OID on field %s", f.Name)
   144  		}
   145  		var oid string
   146  		if f.Oid[0] == '.' {
   147  			oid = f.Oid
   148  		} else {
   149  			// make sure OID has "." because the BulkWalkAll results do, and the prefix needs to match
   150  			oid = "." + f.Oid
   151  		}
   152  
   153  		// ifv contains a mapping of table OID index to field value
   154  		ifv := map[string]interface{}{}
   155  
   156  		if !walk {
   157  			// This is used when fetching non-table fields. Fields configured a the top
   158  			// scope of the plugin.
   159  			// We fetch the fields directly, and add them to ifv as if the index were an
   160  			// empty string. This results in all the non-table fields sharing the same
   161  			// index, and being added on the same row.
   162  			if pkt, err := gs.Get([]string{oid}); err != nil {
   163  				if errors.Is(err, gosnmp.ErrUnknownSecurityLevel) {
   164  					return nil, errors.New("unknown security level (sec_level)")
   165  				} else if errors.Is(err, gosnmp.ErrUnknownUsername) {
   166  					return nil, errors.New("unknown username (sec_name)")
   167  				} else if errors.Is(err, gosnmp.ErrWrongDigest) {
   168  					return nil, errors.New("wrong digest (auth_protocol, auth_password)")
   169  				} else if errors.Is(err, gosnmp.ErrDecryption) {
   170  					return nil, errors.New("decryption error (priv_protocol, priv_password)")
   171  				}
   172  				return nil, fmt.Errorf("performing get on field %s: %w", f.Name, err)
   173  			} else if pkt != nil && len(pkt.Variables) > 0 && pkt.Variables[0].Type != gosnmp.NoSuchObject && pkt.Variables[0].Type != gosnmp.NoSuchInstance {
   174  				ent := pkt.Variables[0]
   175  				fv, err := f.Convert(ent)
   176  				if err != nil {
   177  					return nil, fmt.Errorf("converting %q (OID %s) for field %s: %w", ent.Value, ent.Name, f.Name, err)
   178  				}
   179  				ifv[""] = fv
   180  			}
   181  		} else {
   182  			err := gs.Walk(oid, func(ent gosnmp.SnmpPDU) error {
   183  				if len(ent.Name) <= len(oid) || ent.Name[:len(oid)+1] != oid+"." {
   184  					return &walkError{} // break the walk
   185  				}
   186  
   187  				idx := ent.Name[len(oid):]
   188  				if f.OidIndexSuffix != "" {
   189  					if !strings.HasSuffix(idx, f.OidIndexSuffix) {
   190  						// this entry doesn't match our OidIndexSuffix. skip it
   191  						return nil
   192  					}
   193  					idx = idx[:len(idx)-len(f.OidIndexSuffix)]
   194  				}
   195  				if f.OidIndexLength != 0 {
   196  					i := f.OidIndexLength + 1 // leading separator
   197  					idx = strings.Map(func(r rune) rune {
   198  						if r == '.' {
   199  							i--
   200  						}
   201  						if i < 1 {
   202  							return -1
   203  						}
   204  						return r
   205  					}, idx)
   206  				}
   207  
   208  				// snmptranslate table field value here
   209  				if f.Translate {
   210  					if entOid, ok := ent.Value.(string); ok {
   211  						_, _, oidText, _, err := t.translator.SnmpTranslate(entOid)
   212  						if err == nil {
   213  							// If no error translating, the original value for ent.Value should be replaced
   214  							ent.Value = oidText
   215  						}
   216  					}
   217  				}
   218  
   219  				fv, err := f.Convert(ent)
   220  				if err != nil {
   221  					return &walkError{
   222  						msg: fmt.Sprintf("converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name),
   223  						err: err,
   224  					}
   225  				}
   226  				ifv[idx] = fv
   227  				return nil
   228  			})
   229  			if err != nil {
   230  				// Our callback always wraps errors in a walkError.
   231  				// If this error isn't a walkError, we know it's not
   232  				// from the callback
   233  				var walkErr *walkError
   234  				if !errors.As(err, &walkErr) {
   235  					return nil, fmt.Errorf("performing bulk walk for field %s: %w", f.Name, err)
   236  				}
   237  			}
   238  		}
   239  
   240  		for idx, v := range ifv {
   241  			if f.SecondaryIndexUse {
   242  				if newidx, ok := secIdxTab[idx]; ok {
   243  					idx = newidx
   244  				} else {
   245  					if !secGlobalOuterJoin && !f.SecondaryOuterJoin {
   246  						continue
   247  					}
   248  					idx = ".Secondary" + idx
   249  				}
   250  			}
   251  			rtr, ok := rows[idx]
   252  			if !ok {
   253  				rtr = RTableRow{}
   254  				rtr.Tags = map[string]string{}
   255  				rtr.Fields = map[string]interface{}{}
   256  				rows[idx] = rtr
   257  			}
   258  			if t.IndexAsTag && idx != "" {
   259  				if idx[0] == '.' {
   260  					idx = idx[1:]
   261  				}
   262  				rtr.Tags["index"] = idx
   263  			}
   264  			// don't add an empty string
   265  			if vs, ok := v.(string); !ok || vs != "" {
   266  				if f.IsTag {
   267  					if ok {
   268  						rtr.Tags[f.Name] = vs
   269  					} else {
   270  						rtr.Tags[f.Name] = fmt.Sprintf("%v", v)
   271  					}
   272  				} else {
   273  					rtr.Fields[f.Name] = v
   274  				}
   275  				if f.SecondaryIndexTable {
   276  					//indexes are stored here with prepending "." so we need to add them if needed
   277  					var vss string
   278  					if ok {
   279  						vss = "." + vs
   280  					} else {
   281  						vss = fmt.Sprintf(".%v", v)
   282  					}
   283  					if idx[0] == '.' {
   284  						secIdxTab[vss] = idx
   285  					} else {
   286  						secIdxTab[vss] = "." + idx
   287  					}
   288  				}
   289  			}
   290  		}
   291  	}
   292  
   293  	rt := RTable{
   294  		Name: t.Name,
   295  		Time: time.Now(), //TODO record time at start
   296  		Rows: make([]RTableRow, 0, len(rows)),
   297  	}
   298  	for _, r := range rows {
   299  		rt.Rows = append(rt.Rows, r)
   300  	}
   301  	return &rt, nil
   302  }
   303  
   304  type walkError struct {
   305  	msg string
   306  	err error
   307  }
   308  
   309  func (e *walkError) Error() string {
   310  	return e.msg
   311  }
   312  
   313  func (e *walkError) Unwrap() error {
   314  	return e.err
   315  }