diff --git a/cmd/ktranslate/main.go b/cmd/ktranslate/main.go index 46be1ca9..f9049399 100644 --- a/cmd/ktranslate/main.go +++ b/cmd/ktranslate/main.go @@ -696,6 +696,8 @@ func applyFlags(cfg *ktranslate.Config) error { return } cfg.SNMPInput.ValidateMIBs = v + case "snmp_walk_file": + cfg.SNMPInput.RunFromWalkFile = val // pkg/inputs/vpc/gcp case "gcp.project": cfg.GCPVPCInput.Enable = true diff --git a/config.go b/config.go index be91c03c..d960c68e 100644 --- a/config.go +++ b/config.go @@ -190,6 +190,7 @@ type SNMPInputConfig struct { DiscoveryIntervalMinutes int DiscoveryOnStart bool ValidateMIBs bool + RunFromWalkFile string PollNowTarget string } @@ -491,6 +492,7 @@ func DefaultConfig() *Config { DiscoveryIntervalMinutes: 0, DiscoveryOnStart: false, ValidateMIBs: false, + RunFromWalkFile: "", }, GCPVPCInput: &GCPVPCInputConfig{ Enable: false, diff --git a/pkg/inputs/snmp/snmp.go b/pkg/inputs/snmp/snmp.go index 49713697..7f565fbf 100644 --- a/pkg/inputs/snmp/snmp.go +++ b/pkg/inputs/snmp/snmp.go @@ -30,17 +30,18 @@ var ( mibdb *mibs.MibDB // Global singleton instance here. ServiceName = "" - dumpMibTable bool - flowOnly bool - jsonToYaml string - snmpWalk string - snmpWalkOid string - snmpWalkFormat string - snmpOutFile string - snmpPollNow string - snmpDiscoDur int - snmpDiscoSt bool - validateMib bool + dumpMibTable bool + flowOnly bool + jsonToYaml string + snmpWalk string + snmpWalkOid string + snmpWalkFormat string + snmpOutFile string + snmpPollNow string + snmpDiscoDur int + snmpDiscoSt bool + validateMib bool + runFromWalkFile string ) func init() { @@ -55,6 +56,7 @@ func init() { flag.IntVar(&snmpDiscoDur, "snmp_discovery_min", 0, "If set, run snmp discovery on this interval (in minutes).") flag.BoolVar(&snmpDiscoSt, "snmp_discovery_on_start", false, "If set, run snmp discovery on application start.") flag.BoolVar(&validateMib, "snmp_validate", false, "If true, validate mib profiles and exit.") + flag.StringVar(&runFromWalkFile, "snmp_walk_file", "", "If set, use the walk file instead of polling.") } func StartSNMPPolls(ctx context.Context, jchfChan chan []*kt.JCHF, metrics *kt.SnmpMetricSet, registry go_metrics.Registry, apic *api.KentikApi, log logger.ContextL, cfg *ktranslate.SNMPInputConfig, resolv *resolv.Resolver, confMgr config.ConfigManager, logchan chan string) error { @@ -73,6 +75,13 @@ func StartSNMPPolls(ctx context.Context, jchfChan chan []*kt.JCHF, metrics *kt.S return snmp_util.DoWalk(v, cfg.WalkOID, cfg.WalkFormat, conf, connectTimeout, retries, log) } + if v := cfg.RunFromWalkFile; v != "" { // If this flag is set, tell the util function to return responses based only on the walk. + err := snmp_util.LoadFromWalk(ctx, cfg.RunFromWalkFile, log) + if err != nil { + return err + } + } + // Load a mibdb if we have one. if conf.Global != nil { mdb, err := mibs.NewMibDB(conf.Global.MibDB, conf.Global.MibProfileDir, cfg.ValidateMIBs, log) diff --git a/pkg/inputs/snmp/util/util.go b/pkg/inputs/snmp/util/util.go index cfde9bdf..e3c8720c 100644 --- a/pkg/inputs/snmp/util/util.go +++ b/pkg/inputs/snmp/util/util.go @@ -58,6 +58,8 @@ var ( TRUNCATE = true NO_TRUNCATE = false MAX_SNMP_LEN = 128 + + walkCacheMap map[string]gosnmp.SnmpPDU ) func ReadOctetString(variable gosnmp.SnmpPDU, truncate bool) (string, bool) { @@ -152,6 +154,11 @@ func WalkOID(ctx context.Context, device *kt.SnmpDeviceConfig, oid string, serve tries = []pollTry{pollTry{walk: walker.WalkAll, sleep: time.Duration(0)}} } else if device.NoUseBulkWalkAll { // If the device says to not use bulkwalkall, trim this out now. tries = tries[1:] + } else if len(walkCacheMap) != 0 { // If we are using canned responses. + tries = []pollTry{ + pollTry{walk: useCachedMap, sleep: time.Duration(0)}, + pollTry{walk: server.BulkWalkAll, sleep: time.Duration(0)}, + pollTry{walk: server.WalkAll, sleep: time.Duration(0)}} } var err error diff --git a/pkg/inputs/snmp/util/walk.go b/pkg/inputs/snmp/util/walk.go new file mode 100644 index 00000000..8552efe5 --- /dev/null +++ b/pkg/inputs/snmp/util/walk.go @@ -0,0 +1,73 @@ +package util + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/kentik/ktranslate/pkg/eggs/logger" + + "github.com/gosnmp/gosnmp" +) + +// Load a snmpwalk response from the given file and use it for debugging. +func LoadFromWalk(ctx context.Context, file string, log logger.ContextL) error { + data, err := os.ReadFile(file) + if err != nil { + return err + } + + res := map[string]gosnmp.SnmpPDU{} + for _, line := range strings.Split(string(data), "\n") { + pts := strings.SplitN(line, " = ", 2) + if len(pts) != 2 { + continue + } + oid := strings.TrimSpace(pts[0]) + if oid[0:1] == "." { // Strip off leading dot. + oid = oid[1:] + } + pp := strings.SplitN(pts[1], ": ", 2) + if len(pp) != 2 { + continue + } + val := pp[1] + + switch strings.ToLower(pp[0]) { + case "string": + res[oid] = gosnmp.SnmpPDU{Value: []byte(val), Type: gosnmp.OctetString, Name: oid} + case "integer": + res[oid] = gosnmp.SnmpPDU{Value: val, Type: gosnmp.Integer, Name: oid} + case "counter64": + res[oid] = gosnmp.SnmpPDU{Value: val, Type: gosnmp.Counter64, Name: oid} + case "counter32": + res[oid] = gosnmp.SnmpPDU{Value: val, Type: gosnmp.Counter32, Name: oid} + default: + log.Errorf("Skipping unknown walk type: %s", pp[0]) + } + } + + walkCacheMap = res + log.Infof("Loaded up %d entries to the walk cache map", len(res)) + + return nil +} + +func useCachedMap(oid string) ([]gosnmp.SnmpPDU, error) { + res := []gosnmp.SnmpPDU{} + + if oid[0:1] == "." { // Strip off leading dot. + oid = oid[1:] + } + for k, v := range walkCacheMap { + if strings.HasPrefix(k, oid) { + res = append(res, v) + } + } + + if len(res) == 0 { + return nil, fmt.Errorf("Nothing to see") + } + return res, nil +}