305 lines
7.6 KiB
Go
305 lines
7.6 KiB
Go
|
package snmp
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/gosnmp/gosnmp"
|
||
|
)
|
||
|
|
||
|
// Table holds the configuration for a SNMP table.
|
||
|
type Table struct {
|
||
|
// Name will be the name of the measurement.
|
||
|
Name string
|
||
|
|
||
|
// Which tags to inherit from the top-level config.
|
||
|
InheritTags []string
|
||
|
|
||
|
// Adds each row's table index as a tag.
|
||
|
IndexAsTag bool
|
||
|
|
||
|
// Fields is the tags and values to look up.
|
||
|
Fields []Field `toml:"field"`
|
||
|
|
||
|
// OID for automatic field population.
|
||
|
// If provided, init() will populate Fields with all the table columns of the
|
||
|
// given OID.
|
||
|
Oid string
|
||
|
|
||
|
initialized bool
|
||
|
translator Translator
|
||
|
}
|
||
|
|
||
|
// RTable is the resulting table built from a Table.
|
||
|
type RTable struct {
|
||
|
// Name is the name of the field, copied from Table.Name.
|
||
|
Name string
|
||
|
// Time is the time the table was built.
|
||
|
Time time.Time
|
||
|
// Rows are the rows that were found, one row for each table OID index found.
|
||
|
Rows []RTableRow
|
||
|
}
|
||
|
|
||
|
// RTableRow is the resulting row containing all the OID values which shared
|
||
|
// the same index.
|
||
|
type RTableRow struct {
|
||
|
// Tags are all the Field values which had IsTag=true.
|
||
|
Tags map[string]string
|
||
|
// Fields are all the Field values which had IsTag=false.
|
||
|
Fields map[string]interface{}
|
||
|
}
|
||
|
|
||
|
// Init builds & initializes the nested fields.
|
||
|
func (t *Table) Init(tr Translator) error {
|
||
|
// makes sure oid or name is set in config file
|
||
|
// otherwise snmp will produce metrics with an empty name
|
||
|
if t.Oid == "" && t.Name == "" {
|
||
|
return errors.New("unnamed SNMP table in config file: one or both of the oid and name settings must be set")
|
||
|
}
|
||
|
|
||
|
if t.initialized {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
t.translator = tr
|
||
|
if err := t.initBuild(); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
secondaryIndexTablePresent := false
|
||
|
// initialize all the nested fields
|
||
|
for i := range t.Fields {
|
||
|
if err := t.Fields[i].Init(t.translator); err != nil {
|
||
|
return fmt.Errorf("initializing field %s: %w", t.Fields[i].Name, err)
|
||
|
}
|
||
|
if t.Fields[i].SecondaryIndexTable {
|
||
|
if secondaryIndexTablePresent {
|
||
|
return errors.New("only one field can be SecondaryIndexTable")
|
||
|
}
|
||
|
secondaryIndexTablePresent = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
t.initialized = true
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// initBuild initializes the table if it has an OID configured. If so, the
|
||
|
// net-snmp tools will be used to look up the OID and auto-populate the table's
|
||
|
// fields.
|
||
|
func (t *Table) initBuild() error {
|
||
|
if t.Oid == "" {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
_, _, oidText, fields, err := t.translator.SnmpTable(t.Oid)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if t.Name == "" {
|
||
|
t.Name = oidText
|
||
|
}
|
||
|
|
||
|
knownOIDs := make(map[string]bool, len(t.Fields))
|
||
|
for _, f := range t.Fields {
|
||
|
knownOIDs[f.Oid] = true
|
||
|
}
|
||
|
for _, f := range fields {
|
||
|
if !knownOIDs[f.Oid] {
|
||
|
t.Fields = append(t.Fields, f)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Build retrieves all the fields specified in the table and constructs the RTable.
|
||
|
func (t Table) Build(gs Connection, walk bool) (*RTable, error) {
|
||
|
rows := make(map[string]RTableRow)
|
||
|
|
||
|
// translation table for secondary index (when performing join on two tables)
|
||
|
secIdxTab := make(map[string]string)
|
||
|
secGlobalOuterJoin := false
|
||
|
for i, f := range t.Fields {
|
||
|
if f.SecondaryIndexTable {
|
||
|
secGlobalOuterJoin = f.SecondaryOuterJoin
|
||
|
if i != 0 {
|
||
|
t.Fields[0], t.Fields[i] = t.Fields[i], t.Fields[0]
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
tagCount := 0
|
||
|
for _, f := range t.Fields {
|
||
|
if f.IsTag {
|
||
|
tagCount++
|
||
|
}
|
||
|
|
||
|
if len(f.Oid) == 0 {
|
||
|
return nil, fmt.Errorf("cannot have empty OID on field %s", f.Name)
|
||
|
}
|
||
|
var oid string
|
||
|
if f.Oid[0] == '.' {
|
||
|
oid = f.Oid
|
||
|
} else {
|
||
|
// make sure OID has "." because the BulkWalkAll results do, and the prefix needs to match
|
||
|
oid = "." + f.Oid
|
||
|
}
|
||
|
|
||
|
// ifv contains a mapping of table OID index to field value
|
||
|
ifv := make(map[string]interface{})
|
||
|
|
||
|
if !walk {
|
||
|
// This is used when fetching non-table fields. Fields configured a the top
|
||
|
// scope of the plugin.
|
||
|
// We fetch the fields directly, and add them to ifv as if the index were an
|
||
|
// empty string. This results in all the non-table fields sharing the same
|
||
|
// index, and being added on the same row.
|
||
|
if pkt, err := gs.Get([]string{oid}); err != nil {
|
||
|
if errors.Is(err, gosnmp.ErrUnknownSecurityLevel) {
|
||
|
return nil, errors.New("unknown security level (sec_level)")
|
||
|
} else if errors.Is(err, gosnmp.ErrUnknownUsername) {
|
||
|
return nil, errors.New("unknown username (sec_name)")
|
||
|
} else if errors.Is(err, gosnmp.ErrWrongDigest) {
|
||
|
return nil, errors.New("wrong digest (auth_protocol, auth_password)")
|
||
|
} else if errors.Is(err, gosnmp.ErrDecryption) {
|
||
|
return nil, errors.New("decryption error (priv_protocol, priv_password)")
|
||
|
}
|
||
|
return nil, fmt.Errorf("performing get on field %s: %w", f.Name, err)
|
||
|
} else if pkt != nil && len(pkt.Variables) > 0 && pkt.Variables[0].Type != gosnmp.NoSuchObject && pkt.Variables[0].Type != gosnmp.NoSuchInstance {
|
||
|
ent := pkt.Variables[0]
|
||
|
fv, err := f.Convert(ent)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("converting %q (OID %s) for field %s: %w", ent.Value, ent.Name, f.Name, err)
|
||
|
}
|
||
|
ifv[""] = fv
|
||
|
}
|
||
|
} else {
|
||
|
err := gs.Walk(oid, func(ent gosnmp.SnmpPDU) error {
|
||
|
if len(ent.Name) <= len(oid) || ent.Name[:len(oid)+1] != oid+"." {
|
||
|
return &walkError{} // break the walk
|
||
|
}
|
||
|
|
||
|
idx := ent.Name[len(oid):]
|
||
|
if f.OidIndexSuffix != "" {
|
||
|
if !strings.HasSuffix(idx, f.OidIndexSuffix) {
|
||
|
// this entry doesn't match our OidIndexSuffix. skip it
|
||
|
return nil
|
||
|
}
|
||
|
idx = idx[:len(idx)-len(f.OidIndexSuffix)]
|
||
|
}
|
||
|
if f.OidIndexLength != 0 {
|
||
|
i := f.OidIndexLength + 1 // leading separator
|
||
|
idx = strings.Map(func(r rune) rune {
|
||
|
if r == '.' {
|
||
|
i--
|
||
|
}
|
||
|
if i < 1 {
|
||
|
return -1
|
||
|
}
|
||
|
return r
|
||
|
}, idx)
|
||
|
}
|
||
|
|
||
|
fv, err := f.Convert(ent)
|
||
|
if err != nil {
|
||
|
return &walkError{
|
||
|
msg: fmt.Sprintf("converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name),
|
||
|
err: err,
|
||
|
}
|
||
|
}
|
||
|
ifv[idx] = fv
|
||
|
return nil
|
||
|
})
|
||
|
if err != nil {
|
||
|
// Our callback always wraps errors in a walkError.
|
||
|
// If this error isn't a walkError, we know it's not
|
||
|
// from the callback
|
||
|
var walkErr *walkError
|
||
|
if !errors.As(err, &walkErr) {
|
||
|
return nil, fmt.Errorf("performing bulk walk for field %s: %w", f.Name, err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for idx, v := range ifv {
|
||
|
if f.SecondaryIndexUse {
|
||
|
if newidx, ok := secIdxTab[idx]; ok {
|
||
|
idx = newidx
|
||
|
} else {
|
||
|
if !secGlobalOuterJoin && !f.SecondaryOuterJoin {
|
||
|
continue
|
||
|
}
|
||
|
idx = ".Secondary" + idx
|
||
|
}
|
||
|
}
|
||
|
rtr, ok := rows[idx]
|
||
|
if !ok {
|
||
|
rtr = RTableRow{}
|
||
|
rtr.Tags = make(map[string]string)
|
||
|
rtr.Fields = make(map[string]interface{})
|
||
|
rows[idx] = rtr
|
||
|
}
|
||
|
if t.IndexAsTag && idx != "" {
|
||
|
if idx[0] == '.' {
|
||
|
idx = idx[1:]
|
||
|
}
|
||
|
rtr.Tags["index"] = idx
|
||
|
}
|
||
|
// don't add an empty string
|
||
|
if vs, ok := v.(string); !ok || vs != "" {
|
||
|
if f.IsTag {
|
||
|
if ok {
|
||
|
rtr.Tags[f.Name] = vs
|
||
|
} else {
|
||
|
rtr.Tags[f.Name] = fmt.Sprintf("%v", v)
|
||
|
}
|
||
|
} else {
|
||
|
rtr.Fields[f.Name] = v
|
||
|
}
|
||
|
if f.SecondaryIndexTable {
|
||
|
// indexes are stored here with prepending "." so we need to add them if needed
|
||
|
var vss string
|
||
|
if ok {
|
||
|
vss = "." + vs
|
||
|
} else {
|
||
|
vss = fmt.Sprintf(".%v", v)
|
||
|
}
|
||
|
if idx[0] == '.' {
|
||
|
secIdxTab[vss] = idx
|
||
|
} else {
|
||
|
secIdxTab[vss] = "." + idx
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
rt := RTable{
|
||
|
Name: t.Name,
|
||
|
Time: time.Now(), // TODO record time at start
|
||
|
Rows: make([]RTableRow, 0, len(rows)),
|
||
|
}
|
||
|
for _, r := range rows {
|
||
|
rt.Rows = append(rt.Rows, r)
|
||
|
}
|
||
|
return &rt, nil
|
||
|
}
|
||
|
|
||
|
type walkError struct {
|
||
|
msg string
|
||
|
err error
|
||
|
}
|
||
|
|
||
|
func (e *walkError) Error() string {
|
||
|
return e.msg
|
||
|
}
|
||
|
|
||
|
func (e *walkError) Unwrap() error {
|
||
|
return e.err
|
||
|
}
|