diff --git a/EXTERNAL_PLUGINS.md b/EXTERNAL_PLUGINS.md index 5463ef9201c79..a5529512d99d2 100644 --- a/EXTERNAL_PLUGINS.md +++ b/EXTERNAL_PLUGINS.md @@ -31,7 +31,6 @@ Pull requests welcome. - [fritzbox](https://github.com/hdecarne-github/fritzbox-telegraf-plugin) - Gather statistics from [FRITZ!Box](https://avm.de/produkte/fritzbox/) router and repeater - [linux-psi-telegraf-plugin](https://github.com/gridscale/linux-psi-telegraf-plugin) - Gather pressure stall information ([PSI](https://facebookmicrosites.github.io/psi/)) from the Linux Kernel - [huebridge](https://github.com/hdecarne-github/huebridge-telegraf-plugin) - Gather smart home statistics from [Hue Bridge](https://www.philips-hue.com/) devices -- [nsdp](https://github.com/hdecarne-github/nsdp-telegraf-plugin) - Gather switch network statistics via [Netgear Switch Discovery Protocol](https://en.wikipedia.org/wiki/Netgear_Switch_Discovery_Protocol) - [hwinfo](https://github.com/zachstence/hwinfo-telegraf-plugin) - Gather Windows system hardware information from [HWiNFO](https://www.hwinfo.com/) - [libvirt](https://gitlab.com/warrenio/tools/telegraf-input-libvirt) - Gather libvirt domain stats, based on a historical Telegraf implementation [libvirt](https://libvirt.org/) - [bacnet](https://github.com/JurajMarcin/telegraf-bacnet) - Gather statistics from BACnet devices, with support for device discovery and Change of Value subscriptions diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md index b8994365b6154..630777aeee17c 100644 --- a/docs/LICENSE_OF_DEPENDENCIES.md +++ b/docs/LICENSE_OF_DEPENDENCIES.md @@ -362,6 +362,7 @@ following works: - github.com/stoewer/go-strcase [MIT License](https://github.com/stoewer/go-strcase/blob/master/LICENSE) - github.com/stretchr/objx [MIT License](https://github.com/stretchr/objx/blob/master/LICENSE) - github.com/stretchr/testify [MIT License](https://github.com/stretchr/testify/blob/master/LICENSE) +- github.com/tdrn-org/go-nsdp [MIT License](https://github.com/tdrn-org/go-nsdp/blob/main/LICENSE) - github.com/testcontainers/testcontainers-go [MIT License](https://github.com/testcontainers/testcontainers-go/blob/main/LICENSE) - github.com/thomasklein94/packer-plugin-libvirt [Mozilla Public License 2.0](https://github.com/thomasklein94/packer-plugin-libvirt/blob/main/LICENSE) - github.com/tidwall/gjson [MIT License](https://github.com/tidwall/gjson/blob/master/LICENSE) diff --git a/go.mod b/go.mod index 27e54af0c7586..e93bd5d104135 100644 --- a/go.mod +++ b/go.mod @@ -471,6 +471,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/tdrn-org/go-nsdp v0.5.0 github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/tinylru v1.2.1 // indirect diff --git a/go.sum b/go.sum index ba6c4e2b06eeb..cc2ea9f823447 100644 --- a/go.sum +++ b/go.sum @@ -2321,6 +2321,8 @@ github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7 h1:Jtcrb09q0AVWe3 github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA= github.com/tbrandon/mbserver v0.0.0-20170611213546-993e1772cc62 h1:Oj2e7Sae4XrOsk3ij21QjjEgAcVSeo9nkp0dI//cD2o= github.com/tbrandon/mbserver v0.0.0-20170611213546-993e1772cc62/go.mod h1:qUzPVlSj2UgxJkVbH0ZwuuiR46U8RBMDT5KLY78Ifpw= +github.com/tdrn-org/go-nsdp v0.5.0 h1:bOs8qABaP/BSQlWeziZx9gjGkC2ld9UQek9p5w6PvdY= +github.com/tdrn-org/go-nsdp v0.5.0/go.mod h1:zp7CxiCPcyXHo+s6tn+wrNBr1qQe1G/hOh/FybM5xiM= github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= diff --git a/plugins/inputs/all/nsdp.go b/plugins/inputs/all/nsdp.go new file mode 100644 index 0000000000000..9921dcd2ec8fa --- /dev/null +++ b/plugins/inputs/all/nsdp.go @@ -0,0 +1,5 @@ +//go:build !custom || inputs || inputs.nsdp + +package all + +import _ "github.com/influxdata/telegraf/plugins/inputs/nsdp" // register plugin diff --git a/plugins/inputs/nsdp/README.md b/plugins/inputs/nsdp/README.md new file mode 100644 index 0000000000000..c0f07bc88e9ae --- /dev/null +++ b/plugins/inputs/nsdp/README.md @@ -0,0 +1,63 @@ +# Netgear Switch Discovery Protocol Input Plugin + +This plugin gathers metrics from devices via +[Netgear Switch Discovery Protocol (NSDP)][nsdp] +for all available switches and ports. + +⭐ Telegraf v1.34.0 +🏷️ network +💻 all + +[nsdp]: https://en.wikipedia.org/wiki/Netgear_Switch_Discovery_Protocol + +## Global configuration options + +In addition to the plugin-specific configuration settings, plugins support +additional global and plugin configuration settings. These settings are used to +modify metrics, tags, and field or create aliases and configure ordering, etc. +See the [CONFIGURATION.md][CONFIGURATION.md] for more details. + +[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins + +## Configuration + +```toml @sample.conf +# Gather Netgear Switch Discovery Protocol status +[[inputs.nsdp]] + ## The target address to use for status gathering. Either Broadcast (default) + ## or the address of a single well-known device. + # address = "255.255.255.255:63322" + + ## The maximum number of device responses to wait for. 0 means no limit. + ## NSDP works asynchronously. Without a limit (0) the plugin always waits + ## the amount given in timeout for possible responses. By setting this + ## option to the known number of devices, the plugin completes + ## processing as soon as the last device has answered. + # device_limit = 0 + + ## The maximum duration to wait for device responses. + # timeout = "2s" +``` + +## Metrics + +- `nsdp_device_port` + - tags + - `device` - The device identifier (MAC/HW address) + - `device_ip` - The device's IP address + - `device_name` - The device's name + - `device_model` - The device's model + - `device_port` - The port id the fields are referring to + - fields + - `bytes_sent` (uint) - Number of bytes sent via this port + - `bytes_recv` (uint) - Number of bytes received via this port + - `packets_total` (uint) - Total number of packets processed on this port + - `broadcasts_total` (uint) - Total number of broadcasts processed on this port + - `multicasts_total` (uint) - Total number of multicasts processed on this port + - `errors_total` (uint) - Total number of errors encountered on this port + +## Example Output + +```text +nsdp_device_port,device=12:34:56:78:9a:bc,device_ip=10.1.0.4,device_model=GS108Ev3,device_name=switch2,device_port=1 broadcasts_total=0u,bytes_recv=3879427866u,bytes_sent=506548796u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014578000 +``` diff --git a/plugins/inputs/nsdp/nsdp.go b/plugins/inputs/nsdp/nsdp.go new file mode 100644 index 0000000000000..8208d2ee5f065 --- /dev/null +++ b/plugins/inputs/nsdp/nsdp.go @@ -0,0 +1,134 @@ +//go:generate ../../../tools/readme_config_includer/generator +package nsdp + +import ( + _ "embed" + "fmt" + "net" + "strconv" + "time" + + "github.com/tdrn-org/go-nsdp" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/config" + "github.com/influxdata/telegraf/plugins/inputs" +) + +//go:embed sample.conf +var sampleConfig string + +type NSDP struct { + Address string `toml:"address"` + DeviceLimit uint `toml:"device_limit"` + Timeout config.Duration `toml:"timeout"` + + Log telegraf.Logger `toml:"-"` + + conn *nsdp.Conn +} + +func (*NSDP) SampleConfig() string { + return sampleConfig +} + +func (n *NSDP) Init() error { + if n.Address == "" { + n.Address = nsdp.IPv4BroadcastTarget + } + if n.Timeout <= 0 { + return fmt.Errorf("invalid Timeout value %d, must be greater 0", n.Timeout) + } + return nil +} + +func (n *NSDP) Start(telegraf.Accumulator) error { + conn, err := nsdp.NewConn(n.Address, n.Log.Level().Includes(telegraf.Trace)) + if err != nil { + return fmt.Errorf("failed to create connection to address %s: %s", n.Address, err) + } + conn.ReceiveDeviceLimit = n.DeviceLimit + conn.ReceiveTimeout = time.Duration(n.Timeout) + n.conn = conn + return nil +} + +func (n *NSDP) Stop() { + if n.conn == nil { + return + } + n.conn.Close() + n.conn = nil +} + +func (n *NSDP) Gather(acc telegraf.Accumulator) error { + if n.conn == nil { + if err := n.Start(nil); err != nil { + return err + } + } + + // Send request to query devices including infos (model, name, IP) and status (port statistics) + request := nsdp.NewMessage(nsdp.ReadRequest) + request.AppendTLV(nsdp.EmptyDeviceModel()) + request.AppendTLV(nsdp.EmptyDeviceName()) + request.AppendTLV(nsdp.EmptyDeviceIP()) + request.AppendTLV(nsdp.EmptyPortStatistic()) + responses, err := n.conn.SendReceiveMessage(request) + if err != nil { + // Close malfunctioning connection and re-connect on next Gather call + n.Stop() + return fmt.Errorf("failed to query address %s: %w", n.Address, err) + } + + // Create metrics for each responding device + for device, response := range responses { + n.Log.Tracef("Processing device: %s", device) + n.gatherDevice(acc, device, response) + } + return nil +} + +func (n *NSDP) gatherDevice(acc telegraf.Accumulator, device string, response *nsdp.Message) { + var deviceModel string + var deviceName string + var deviceIP net.IP + portStats := make(map[uint8]*nsdp.PortStatistic, 0) + for _, tlv := range response.Body { + switch tlv.Type() { + case nsdp.TypeDeviceModel: + deviceModel = tlv.(*nsdp.DeviceModel).Model + case nsdp.TypeDeviceName: + deviceName = tlv.(*nsdp.DeviceName).Name + case nsdp.TypeDeviceIP: + deviceIP = tlv.(*nsdp.DeviceIP).IP + case nsdp.TypePortStatistic: + portStat := tlv.(*nsdp.PortStatistic) + portStats[portStat.Port] = portStat + } + } + for port, stat := range portStats { + tags := map[string]string{ + "device": device, + "device_ip": deviceIP.String(), + "device_name": deviceName, + "device_model": deviceModel, + "device_port": strconv.FormatUint(uint64(port), 10), + } + fields := map[string]interface{}{ + "bytes_sent": stat.Sent, + "bytes_recv": stat.Received, + "packets_total": stat.Packets, + "broadcasts_total": stat.Broadcasts, + "multicasts_total": stat.Multicasts, + "errors_total": stat.Errors, + } + acc.AddCounter("nsdp_device_port", fields, tags) + } +} + +func init() { + inputs.Add("nsdp", func() telegraf.Input { + return &NSDP{Timeout: config.Duration(2 * time.Second)} + }) +} diff --git a/plugins/inputs/nsdp/nsdp_test.go b/plugins/inputs/nsdp/nsdp_test.go new file mode 100644 index 0000000000000..c027f798b1779 --- /dev/null +++ b/plugins/inputs/nsdp/nsdp_test.go @@ -0,0 +1,81 @@ +package nsdp + +import ( + "testing" + "time" + + "github.com/tdrn-org/go-nsdp" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/config" + "github.com/influxdata/telegraf/plugins/parsers/influx" + "github.com/influxdata/telegraf/testutil" + "github.com/stretchr/testify/require" +) + +func TestLoadConfig(t *testing.T) { + // Verify plugin can be loaded from config + conf := config.NewConfig() + require.NoError(t, conf.LoadConfig("testdata/conf/nsdp.conf")) + require.Len(t, conf.Inputs, 1) + plugin, ok := conf.Inputs[0].Input.(*NSDP) + require.True(t, ok) + + // Verify successful Init + require.NoError(t, plugin.Init()) + + // Verify everything is setup according to config file + require.Equal(t, "127.0.0.1:63322", plugin.Address) + require.Equal(t, uint(1), plugin.DeviceLimit) + require.Equal(t, config.Duration(5*time.Second), plugin.Timeout) +} + +func TestInvalidTimeoutConfig(t *testing.T) { + plugin := &NSDP{ + Timeout: config.Duration(0 * time.Second), + } + + // Verify failing Init + require.EqualError(t, plugin.Init(), "invalid Timeout value 0, must be greater 0") +} + +func TestGather(t *testing.T) { + // Setup and start test responder + responder, err := nsdp.NewTestResponder("localhost:0") + require.NoError(t, err) + defer responder.Stop() + responder.AddResponses( + "0102000000000000bcd07432b8dc123456789abc000037b94e534450000000000001000847533130384576330003000773776974636832000600040a010004100000310100000000e73b5f1a000000001e31523c0000000000000000000000000000000000000000000000000000000000000000100000310200000000152d5eae0000000052ea11ea0000000000000000000000000000000000000000000000000000000000000000100000310300000000068561aa00000000bcc8cb35000000000000000000000000000000000000000000000000000000000000000010000031040000000002d5fe00000000002b37dad900000000000000000000000000000000000000000000000000000000000000001000003105000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000310600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000031070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000003108000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff0000", + "0102000000000000bcd07432b8dccba987654321000037b94e534450000000000001000847533130384576330003000773776974636831000600040a0100031000003101000000059a9d833200000000303e8eb5000000000000000000000000000000000000000000000000000000000000000010000031020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000003103000000000d9a35e4000000026523c66600000000000000000000000000000000000000000000000000000000000000001000003104000000000041c7530000000002cd94ba000000000000000000000000000000000000000000000000000000000000000010000031050000000021b9ca41000000031a9bff610000000000000000000000000000000000000000000000000000000000000000100000310600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000031070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000003108000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffff0000") + require.NoError(t, responder.Start()) + + // Setup the plugin to target the test responder + plugin := &NSDP{ + Address: responder.Target(), + DeviceLimit: 2, + Timeout: config.Duration(2 * time.Second), + Log: testutil.Logger{Name: "nsdp"}, + } + + // Verify successful Init + require.NoError(t, plugin.Init()) + + // Verify successfull Gather + var acc testutil.Accumulator + require.NoError(t, acc.GatherError(plugin.Gather)) + + // Verify collected metrics are as expected + expectedMetrics := loadExpectedMetrics(t, "testdata/metrics/nsdp_device_port.txt", telegraf.Counter) + testutil.RequireMetricsEqual(t, expectedMetrics, acc.GetTelegrafMetrics(), testutil.IgnoreTime(), testutil.SortMetrics()) +} + +func loadExpectedMetrics(t *testing.T, file string, vt telegraf.ValueType) []telegraf.Metric { + parser := &influx.Parser{} + require.NoError(t, parser.Init()) + expectedMetrics, err := testutil.ParseMetricsFromFile(file, parser) + require.NoError(t, err) + for index := range expectedMetrics { + expectedMetrics[index].SetType(vt) + } + return expectedMetrics +} diff --git a/plugins/inputs/nsdp/sample.conf b/plugins/inputs/nsdp/sample.conf new file mode 100644 index 0000000000000..e9990227a7d16 --- /dev/null +++ b/plugins/inputs/nsdp/sample.conf @@ -0,0 +1,15 @@ +# Gather Netgear Switch Discovery Protocol status +[[inputs.nsdp]] + ## The target address to use for status gathering. Either Broadcast (default) + ## or the address of a single well-known device. + # address = "255.255.255.255:63322" + + ## The maximum number of device responses to wait for. 0 means no limit. + ## NSDP works asynchronously. Without a limit (0) the plugin always waits + ## the amount given in timeout for possible responses. By setting this + ## option to the known number of devices, the plugin completes + ## processing as soon as the last device has answered. + # device_limit = 0 + + ## The maximum duration to wait for device responses. + # timeout = "2s" diff --git a/plugins/inputs/nsdp/testdata/conf/nsdp.conf b/plugins/inputs/nsdp/testdata/conf/nsdp.conf new file mode 100644 index 0000000000000..d3d4bccf77eaa --- /dev/null +++ b/plugins/inputs/nsdp/testdata/conf/nsdp.conf @@ -0,0 +1,15 @@ +# Gather Netgear Switch Discovery Protocol status +[[inputs.nsdp]] + ## The target address to use for status gathering. Either Broadcast (default) + ## or the address of a single well-known device. + address = "127.0.0.1:63322" + + ## The maximum number of device responses to wait for. 0 means no limit. + ## NSDP works asynchronously. Without a limit (0) the plugin always waits + ## the amount given in timeout for possible responses. By setting this + ## option to the known number of devices, the plugin completes + ## processing as soon as the last device has answered. + device_limit = 1 + + ## The maximum duration to wait for device responses. + timeout = "5s" diff --git a/plugins/inputs/nsdp/testdata/metrics/nsdp_device_port.txt b/plugins/inputs/nsdp/testdata/metrics/nsdp_device_port.txt new file mode 100644 index 0000000000000..f070d33764c45 --- /dev/null +++ b/plugins/inputs/nsdp/testdata/metrics/nsdp_device_port.txt @@ -0,0 +1,16 @@ +nsdp_device_port,device=12:34:56:78:9a:bc,device_ip=10.1.0.4,device_model=GS108Ev3,device_name=switch2,device_port=1 broadcasts_total=0u,bytes_recv=3879427866u,bytes_sent=506548796u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014578000 +nsdp_device_port,device=12:34:56:78:9a:bc,device_ip=10.1.0.4,device_model=GS108Ev3,device_name=switch2,device_port=2 broadcasts_total=0u,bytes_recv=355294894u,bytes_sent=1391071722u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014606000 +nsdp_device_port,device=12:34:56:78:9a:bc,device_ip=10.1.0.4,device_model=GS108Ev3,device_name=switch2,device_port=3 broadcasts_total=0u,bytes_recv=109404586u,bytes_sent=3167275829u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014615000 +nsdp_device_port,device=12:34:56:78:9a:bc,device_ip=10.1.0.4,device_model=GS108Ev3,device_name=switch2,device_port=4 broadcasts_total=0u,bytes_recv=47578624u,bytes_sent=725080793u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014624000 +nsdp_device_port,device=12:34:56:78:9a:bc,device_ip=10.1.0.4,device_model=GS108Ev3,device_name=switch2,device_port=5 broadcasts_total=0u,bytes_recv=0u,bytes_sent=0u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014624000 +nsdp_device_port,device=12:34:56:78:9a:bc,device_ip=10.1.0.4,device_model=GS108Ev3,device_name=switch2,device_port=6 broadcasts_total=0u,bytes_recv=0u,bytes_sent=0u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014624000 +nsdp_device_port,device=12:34:56:78:9a:bc,device_ip=10.1.0.4,device_model=GS108Ev3,device_name=switch2,device_port=7 broadcasts_total=0u,bytes_recv=0u,bytes_sent=0u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014624000 +nsdp_device_port,device=12:34:56:78:9a:bc,device_ip=10.1.0.4,device_model=GS108Ev3,device_name=switch2,device_port=8 broadcasts_total=0u,bytes_recv=0u,bytes_sent=0u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014624000 +nsdp_device_port,device=cb:a9:87:65:43:21,device_ip=10.1.0.3,device_model=GS108Ev3,device_name=switch1,device_port=1 broadcasts_total=0u,bytes_recv=24068850482u,bytes_sent=809406133u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014647000 +nsdp_device_port,device=cb:a9:87:65:43:21,device_ip=10.1.0.3,device_model=GS108Ev3,device_name=switch1,device_port=2 broadcasts_total=0u,bytes_recv=0u,bytes_sent=0u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014676000 +nsdp_device_port,device=cb:a9:87:65:43:21,device_ip=10.1.0.3,device_model=GS108Ev3,device_name=switch1,device_port=3 broadcasts_total=0u,bytes_recv=228210148u,bytes_sent=10286777958u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014657000 +nsdp_device_port,device=cb:a9:87:65:43:21,device_ip=10.1.0.3,device_model=GS108Ev3,device_name=switch1,device_port=4 broadcasts_total=0u,bytes_recv=4310867u,bytes_sent=47027386u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014668000 +nsdp_device_port,device=cb:a9:87:65:43:21,device_ip=10.1.0.3,device_model=GS108Ev3,device_name=switch1,device_port=5 broadcasts_total=0u,bytes_recv=565824065u,bytes_sent=13331332961u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014676000 +nsdp_device_port,device=cb:a9:87:65:43:21,device_ip=10.1.0.3,device_model=GS108Ev3,device_name=switch1,device_port=6 broadcasts_total=0u,bytes_recv=0u,bytes_sent=0u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014676000 +nsdp_device_port,device=cb:a9:87:65:43:21,device_ip=10.1.0.3,device_model=GS108Ev3,device_name=switch1,device_port=7 broadcasts_total=0u,bytes_recv=0u,bytes_sent=0u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014676000 +nsdp_device_port,device=cb:a9:87:65:43:21,device_ip=10.1.0.3,device_model=GS108Ev3,device_name=switch1,device_port=8 broadcasts_total=0u,bytes_recv=0u,bytes_sent=0u,errors_total=0u,multicasts_total=0u,packets_total=0u 1737152505014676000