diff --git a/plugins/inputs/snmp_trap/snmp_trap.go b/plugins/inputs/snmp_trap/snmp_trap.go index a80276264..80fc28f7c 100644 --- a/plugins/inputs/snmp_trap/snmp_trap.go +++ b/plugins/inputs/snmp_trap/snmp_trap.go @@ -172,7 +172,7 @@ func makeTrapHandler(s *SnmpTrap) handler { var trapOid string if packet.GenericTrap >= 0 && packet.GenericTrap < 6 { - trapOid = "1.3.6.1.6.3.1.1.5." + strconv.Itoa(packet.GenericTrap+1) + trapOid = ".1.3.6.1.6.3.1.1.5." + strconv.Itoa(packet.GenericTrap+1) } else if packet.GenericTrap == 6 { trapOid = packet.Enterprise + ".0." + strconv.Itoa(packet.SpecificTrap) } diff --git a/plugins/inputs/snmp_trap/snmp_trap_test.go b/plugins/inputs/snmp_trap/snmp_trap_test.go index 68121b0c8..34dd6cde0 100644 --- a/plugins/inputs/snmp_trap/snmp_trap_test.go +++ b/plugins/inputs/snmp_trap/snmp_trap_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "strconv" + "strings" "testing" "time" @@ -35,11 +36,15 @@ func TestLoad(t *testing.T) { require.Equal(t, "coldStart", e.oidText) } -func sendTrap(t *testing.T, port uint16) (sentTimestamp uint32) { +func fakeExecCmd(_ internal.Duration, x string, y ...string) ([]byte, error) { + return nil, fmt.Errorf("mock " + x + " " + strings.Join(y, " ")) +} + +func sendTrap(t *testing.T, port uint16, now uint32, trap gosnmp.SnmpTrap, version gosnmp.SnmpVersion) { s := &gosnmp.GoSNMP{ Port: port, Community: "public", - Version: gosnmp.Version2c, + Version: version, Timeout: time.Duration(2) * time.Second, Retries: 3, MaxOids: gosnmp.MaxOids, @@ -52,283 +57,283 @@ func sendTrap(t *testing.T, port uint16) (sentTimestamp uint32) { } defer s.Conn.Close() - // If the first pdu isn't type TimeTicks, gosnmp.SendTrap() will - // prepend one with time.Now(). The time value is part of the - // plugin output so we need to keep track of it and verify it - // later. - now := uint32(time.Now().Unix()) - timePdu := gosnmp.SnmpPDU{ - Name: ".1.3.6.1.2.1.1.3.0", - Type: gosnmp.TimeTicks, - Value: now, - } - - pdu := gosnmp.SnmpPDU{ - Name: ".1.3.6.1.6.3.1.1.4.1.0", // SNMPv2-MIB::snmpTrapOID.0 - Type: gosnmp.ObjectIdentifier, - Value: ".1.3.6.1.6.3.1.1.5.1", // coldStart - } - - trap := gosnmp.SnmpTrap{ - Variables: []gosnmp.SnmpPDU{ - timePdu, - pdu, - }, - } - _, err = s.SendTrap(trap) if err != nil { t.Errorf("SendTrap() err: %v", err) } - - return now } func TestReceiveTrap(t *testing.T) { - // We would prefer to specify port 0 and let the network stack - // choose an unused port for us but TrapListener doesn't have a - // way to return the autoselected port. Instead, we'll use an - // unusual port and hope it's unused. - const port = 12399 - var fakeTime = time.Now() + var now uint32 + now = 123123123 - // hook into the trap handler so the test knows when the trap has - // been received - received := make(chan int) - wrap := func(f handler) handler { - return func(p *gosnmp.SnmpPacket, a *net.UDPAddr) { - f(p, a) - received <- 0 - } + var fakeTime time.Time + fakeTime = time.Unix(456456456, 456) + + type entry struct { + oid string + e mibEntry } - // set up the service input plugin - s := &SnmpTrap{ - ServiceAddress: "udp://:" + strconv.Itoa(port), - makeHandlerWrapper: wrap, - timeFunc: func() time.Time { - return fakeTime + // If the first pdu isn't type TimeTicks, gosnmp.SendTrap() will + // prepend one with time.Now() + var tests = []struct { + name string + + // send + version gosnmp.SnmpVersion + trap gosnmp.SnmpTrap // include pdus + + // recieve + entries []entry + metrics []telegraf.Metric + }{ + //ordinary v2c coldStart trap + { + name: "v2c coldStart", + version: gosnmp.Version2c, + trap: gosnmp.SnmpTrap{ + Variables: []gosnmp.SnmpPDU{ + { + Name: ".1.3.6.1.2.1.1.3.0", + Type: gosnmp.TimeTicks, + Value: now, + }, + { + Name: ".1.3.6.1.6.3.1.1.4.1.0", // SNMPv2-MIB::snmpTrapOID.0 + Type: gosnmp.ObjectIdentifier, + Value: ".1.3.6.1.6.3.1.1.5.1", // coldStart + }, + }, + }, + entries: []entry{ + { + oid: ".1.3.6.1.6.3.1.1.4.1.0", + e: mibEntry{ + "SNMPv2-MIB", + "snmpTrapOID.0", + }, + }, + { + oid: ".1.3.6.1.6.3.1.1.5.1", + e: mibEntry{ + "SNMPv2-MIB", + "coldStart", + }, + }, + { + oid: ".1.3.6.1.2.1.1.3.0", + e: mibEntry{ + "UNUSED_MIB_NAME", + "sysUpTimeInstance", + }, + }, + }, + metrics: []telegraf.Metric{ + testutil.MustMetric( + "snmp_trap", // name + map[string]string{ // tags + "oid": ".1.3.6.1.6.3.1.1.5.1", + "name": "coldStart", + "mib": "SNMPv2-MIB", + "version": "2c", + "source": "127.0.0.1", + }, + map[string]interface{}{ // fields + "sysUpTimeInstance": now, + }, + fakeTime, + ), + }, }, - Log: testutil.Logger{}, - } - require.Nil(t, s.Init()) - var acc testutil.Accumulator - require.Nil(t, s.Start(&acc)) - defer s.Stop() - - // Preload the cache with the oids we'll use in this test so - // snmptranslate and mibs don't need to be installed. - defer s.clear() - s.load(".1.3.6.1.6.3.1.1.4.1.0", - mibEntry{ - "SNMPv2-MIB", - "snmpTrapOID.0", - }) - s.load(".1.3.6.1.6.3.1.1.5.1", - mibEntry{ - "SNMPv2-MIB", - "coldStart", - }) - s.load(".1.3.6.1.2.1.1.3.0", - mibEntry{ - "UNUSED_MIB_NAME", - "sysUpTimeInstance", - }) - - // send the trap - sentTimestamp := sendTrap(t, port) - - // wait for trap to be received - select { - case <-received: - case <-time.After(2 * time.Second): - t.Fatal("timed out waiting for trap to be received") - } - - // verify plugin output - expected := []telegraf.Metric{ - testutil.MustMetric( - "snmp_trap", // name - map[string]string{ // tags - "oid": ".1.3.6.1.6.3.1.1.5.1", - "name": "coldStart", - "mib": "SNMPv2-MIB", - "version": "2c", - "source": "127.0.0.1", + //Check that we're not running snmptranslate to look up oids + //when we shouldn't be. This sends and receives a valid trap + //but metric production should fail because the oids aren't in + //the cache and oid lookup is intentionally mocked to fail. + { + name: "missing oid", + version: gosnmp.Version2c, + trap: gosnmp.SnmpTrap{ + Variables: []gosnmp.SnmpPDU{ + { + Name: ".1.3.6.1.2.1.1.3.0", + Type: gosnmp.TimeTicks, + Value: now, + }, + { + Name: ".1.3.6.1.6.3.1.1.4.1.0", // SNMPv2-MIB::snmpTrapOID.0 + Type: gosnmp.ObjectIdentifier, + Value: ".1.3.6.1.6.3.1.1.5.1", // coldStart + }, + }, }, - map[string]interface{}{ // fields - "sysUpTimeInstance": sentTimestamp, - }, - fakeTime, - ), - } - - testutil.RequireMetricsEqual(t, - expected, acc.GetTelegrafMetrics(), - testutil.SortMetrics()) - -} - -func fakeExecCmd(_ internal.Duration, _ string, _ ...string) ([]byte, error) { - return nil, fmt.Errorf("intentional failure") -} - -func TestMissingOid(t *testing.T) { - // should fail even if snmptranslate is installed - const port = 12399 - var fakeTime = time.Now() - - received := make(chan int) - wrap := func(f handler) handler { - return func(p *gosnmp.SnmpPacket, a *net.UDPAddr) { - f(p, a) - received <- 0 - } - } - - s := &SnmpTrap{ - ServiceAddress: "udp://:" + strconv.Itoa(port), - makeHandlerWrapper: wrap, - timeFunc: func() time.Time { - return fakeTime + entries: []entry{}, //nothing in cache + metrics: []telegraf.Metric{}, }, - Log: testutil.Logger{}, - } - require.Nil(t, s.Init()) - var acc testutil.Accumulator - require.Nil(t, s.Start(&acc)) - defer s.Stop() - - // make sure the cache is empty - s.clear() - - // don't call the real snmptranslate - s.execCmd = fakeExecCmd - - _ = sendTrap(t, port) - - select { - case <-received: - case <-time.After(2 * time.Second): - t.Fatal("timed out waiting for trap to be received") - } - - // oid lookup should fail so we shouldn't get a metric - expected := []telegraf.Metric{} - - testutil.RequireMetricsEqual(t, - expected, acc.GetTelegrafMetrics(), - testutil.SortMetrics()) -} - -func sendV1Trap(t *testing.T, port uint16) (sentTimestamp uint) { - s := &gosnmp.GoSNMP{ - Port: port, - Community: "public", - Version: gosnmp.Version1, - Timeout: time.Duration(2) * time.Second, - Retries: 3, - MaxOids: gosnmp.MaxOids, - Target: "127.0.0.1", - } - - err := s.Connect() - if err != nil { - t.Fatalf("Connect() err: %v", err) - } - defer s.Conn.Close() - - now := uint(time.Now().Unix()) - - pdu := gosnmp.SnmpPDU{ - Name: ".1.2.3.4.5", - Type: gosnmp.OctetString, - Value: "payload", - } - - trap := gosnmp.SnmpTrap{ - Variables: []gosnmp.SnmpPDU{pdu}, - Enterprise: ".1.2.3", - AgentAddress: "10.20.30.40", - GenericTrap: 6, // enterpriseSpecific - SpecificTrap: 55, - Timestamp: now, - } - - _, err = s.SendTrap(trap) - if err != nil { - t.Fatalf("SendTrap() err: %v", err) - } - - return now -} - -func TestReceiveV1Trap(t *testing.T) { - const port = 12399 - var fakeTime = time.Now() - - received := make(chan int) - wrap := func(f handler) handler { - return func(p *gosnmp.SnmpPacket, a *net.UDPAddr) { - f(p, a) - received <- 0 - } - } - - s := &SnmpTrap{ - ServiceAddress: "udp://:" + strconv.Itoa(port), - makeHandlerWrapper: wrap, - timeFunc: func() time.Time { - return fakeTime + //v1 enterprise specific trap + { + name: "v1 trap enterprise", + version: gosnmp.Version1, + trap: gosnmp.SnmpTrap{ + Variables: []gosnmp.SnmpPDU{ + { + Name: ".1.2.3.4.5", + Type: gosnmp.OctetString, + Value: "payload", + }, + }, + Enterprise: ".1.2.3", + AgentAddress: "10.20.30.40", + GenericTrap: 6, // enterpriseSpecific + SpecificTrap: 55, + Timestamp: uint(now), + }, + entries: []entry{ + { + ".1.2.3.4.5", + mibEntry{ + "valueMIB", + "valueOID", + }, + }, + { + ".1.2.3.0.55", + mibEntry{ + "enterpriseMIB", + "enterpriseOID", + }, + }, + }, + metrics: []telegraf.Metric{ + testutil.MustMetric( + "snmp_trap", // name + map[string]string{ // tags + "oid": ".1.2.3.0.55", + "name": "enterpriseOID", + "mib": "enterpriseMIB", + "version": "1", + "source": "127.0.0.1", + "agent_address": "10.20.30.40", + }, + map[string]interface{}{ // fields + "sysUpTimeInstance": uint(now), + "valueOID": "payload", + }, + fakeTime, + ), + }, }, - Log: testutil.Logger{}, - } - require.Nil(t, s.Init()) - var acc testutil.Accumulator - require.Nil(t, s.Start(&acc)) - defer s.Stop() - - defer s.clear() - s.load(".1.2.3.4.5", - mibEntry{ - "valueMIB", - "valueOID", - }) - s.load(".1.2.3.0.55", - mibEntry{ - "enterpriseMIB", - "enterpriseOID", - }) - - sentTimestamp := sendV1Trap(t, port) - - select { - case <-received: - case <-time.After(2 * time.Second): - t.Fatal("timed out waiting for trap to be received") - } - - expected := []telegraf.Metric{ - testutil.MustMetric( - "snmp_trap", // name - map[string]string{ // tags - "oid": ".1.2.3.0.55", - "name": "enterpriseOID", - "mib": "enterpriseMIB", - "version": "1", - "source": "127.0.0.1", - "agent_address": "10.20.30.40", + //v1 generic trap + { + name: "v1 trap generic", + version: gosnmp.Version1, + trap: gosnmp.SnmpTrap{ + Variables: []gosnmp.SnmpPDU{ + { + Name: ".1.2.3.4.5", + Type: gosnmp.OctetString, + Value: "payload", + }, + }, + Enterprise: ".1.2.3", + AgentAddress: "10.20.30.40", + GenericTrap: 0, //coldStart + SpecificTrap: 0, + Timestamp: uint(now), }, - map[string]interface{}{ // fields - "sysUpTimeInstance": sentTimestamp, - "valueOID": "payload", + entries: []entry{ + { + ".1.2.3.4.5", + mibEntry{ + "valueMIB", + "valueOID", + }, + }, + { + ".1.3.6.1.6.3.1.1.5.1", + mibEntry{ + "coldStartMIB", + "coldStartOID", + }, + }, }, - fakeTime, - ), + metrics: []telegraf.Metric{ + testutil.MustMetric( + "snmp_trap", // name + map[string]string{ // tags + "oid": ".1.3.6.1.6.3.1.1.5.1", + "name": "coldStartOID", + "mib": "coldStartMIB", + "version": "1", + "source": "127.0.0.1", + "agent_address": "10.20.30.40", + }, + map[string]interface{}{ // fields + "sysUpTimeInstance": uint(now), + "valueOID": "payload", + }, + fakeTime, + ), + }, + }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We would prefer to specify port 0 and let the network + // stack choose an unused port for us but TrapListener + // doesn't have a way to return the autoselected port. + // Instead, we'll use an unusual port and hope it's + // unused. + const port = 12399 - testutil.RequireMetricsEqual(t, - expected, acc.GetTelegrafMetrics(), - testutil.SortMetrics()) + // Hook into the trap handler so the test knows when the + // trap has been received + received := make(chan int) + wrap := func(f handler) handler { + return func(p *gosnmp.SnmpPacket, a *net.UDPAddr) { + f(p, a) + received <- 0 + } + } + + // Set up the service input plugin + s := &SnmpTrap{ + ServiceAddress: "udp://:" + strconv.Itoa(port), + makeHandlerWrapper: wrap, + timeFunc: func() time.Time { + return fakeTime + }, + Log: testutil.Logger{}, + } + require.Nil(t, s.Init()) + var acc testutil.Accumulator + require.Nil(t, s.Start(&acc)) + defer s.Stop() + + // Preload the cache with the oids we'll use in this test + // so snmptranslate and mibs don't need to be installed. + for _, entry := range tt.entries { + s.load(entry.oid, entry.e) + } + + // Don't look up oid with snmptranslate. + s.execCmd = fakeExecCmd + + // Send the trap + sendTrap(t, port, now, tt.trap, tt.version) + + // Wait for trap to be received + select { + case <-received: + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for trap to be received") + } + + // Verify plugin output + testutil.RequireMetricsEqual(t, + tt.metrics, acc.GetTelegrafMetrics(), + testutil.SortMetrics()) + }) + } }