From 86b21452729d9b4f6a85ce4601b3738b7a583e46 Mon Sep 17 00:00:00 2001 From: Lee Jaeyong Date: Tue, 2 Oct 2018 09:38:13 +0900 Subject: [PATCH] Add support for IPv6 in the ping plugin (#4703) --- plugins/inputs/ping/ping.go | 198 +++++++++++--------- plugins/inputs/ping/ping_test.go | 44 ++++- plugins/inputs/ping/ping_windows.go | 220 ++++++++++++----------- plugins/inputs/ping/ping_windows_test.go | 39 +++- 4 files changed, 303 insertions(+), 198 deletions(-) diff --git a/plugins/inputs/ping/ping.go b/plugins/inputs/ping/ping.go index 430cbe6d4..a95f27ebf 100644 --- a/plugins/inputs/ping/ping.go +++ b/plugins/inputs/ping/ping.go @@ -22,9 +22,11 @@ import ( // HostPinger is a function that runs the "ping" function using a list of // passed arguments. This can be easily switched with a mocked ping function // for unit test purposes (see ping_test.go) -type HostPinger func(timeout float64, args ...string) (string, error) +type HostPinger func(binary string, timeout float64, args ...string) (string, error) type Ping struct { + wg sync.WaitGroup + // Interval at which to ping (ping -i ) PingInterval float64 `toml:"ping_interval"` @@ -43,6 +45,13 @@ type Ping struct { // URLs to ping Urls []string + // Ping executable binary + Binary string + + // Arguments for ping command. + // when `Arguments` is not empty, other options (ping_interval, timeout, etc) will be ignored + Arguments []string + // host ping function pingHost HostPinger } @@ -71,6 +80,13 @@ const sampleConfig = ` ## Interface or source address to send ping from (ping -I ) ## on Darwin and Freebsd only source address possible: (ping -S ) # interface = "" + + ## Specify the ping executable binary, default is "ping" + # binary = "ping" + + ## Arguments for ping command + ## when arguments is not empty, other options (ping_interval, timeout, etc) will be ignored + # arguments = ["-c", "3"] ` func (_ *Ping) SampleConfig() string { @@ -78,90 +94,92 @@ func (_ *Ping) SampleConfig() string { } func (p *Ping) Gather(acc telegraf.Accumulator) error { - - var wg sync.WaitGroup - // Spin off a go routine for each url to ping for _, url := range p.Urls { - wg.Add(1) - go func(u string) { - defer wg.Done() - tags := map[string]string{"url": u} - fields := map[string]interface{}{"result_code": 0} - - _, err := net.LookupHost(u) - if err != nil { - acc.AddError(err) - fields["result_code"] = 1 - acc.AddFields("ping", fields, tags) - return - } - - args := p.args(u, runtime.GOOS) - totalTimeout := float64(p.Count)*p.Timeout + float64(p.Count-1)*p.PingInterval - - out, err := p.pingHost(totalTimeout, args...) - if err != nil { - // Some implementations of ping return a 1 exit code on - // timeout, if this occurs we will not exit and try to parse - // the output. - status := -1 - if exitError, ok := err.(*exec.ExitError); ok { - if ws, ok := exitError.Sys().(syscall.WaitStatus); ok { - status = ws.ExitStatus() - } - } - - if status != 1 { - // Combine go err + stderr output - out = strings.TrimSpace(out) - if len(out) > 0 { - acc.AddError(fmt.Errorf("host %s: %s, %s", u, out, err)) - } else { - acc.AddError(fmt.Errorf("host %s: %s", u, err)) - } - fields["result_code"] = 2 - acc.AddFields("ping", fields, tags) - return - } - } - - trans, rec, min, avg, max, stddev, err := processPingOutput(out) - if err != nil { - // fatal error - acc.AddError(fmt.Errorf("%s: %s", err, u)) - fields["result_code"] = 2 - acc.AddFields("ping", fields, tags) - return - } - // Calculate packet loss percentage - loss := float64(trans-rec) / float64(trans) * 100.0 - fields["packets_transmitted"] = trans - fields["packets_received"] = rec - fields["percent_packet_loss"] = loss - if min >= 0 { - fields["minimum_response_ms"] = min - } - if avg >= 0 { - fields["average_response_ms"] = avg - } - if max >= 0 { - fields["maximum_response_ms"] = max - } - if stddev >= 0 { - fields["standard_deviation_ms"] = stddev - } - acc.AddFields("ping", fields, tags) - }(url) + p.wg.Add(1) + go p.pingToURL(url, acc) } - wg.Wait() + p.wg.Wait() return nil } -func hostPinger(timeout float64, args ...string) (string, error) { - bin, err := exec.LookPath("ping") +func (p *Ping) pingToURL(u string, acc telegraf.Accumulator) { + defer p.wg.Done() + tags := map[string]string{"url": u} + fields := map[string]interface{}{"result_code": 0} + + _, err := net.LookupHost(u) + if err != nil { + acc.AddError(err) + fields["result_code"] = 1 + acc.AddFields("ping", fields, tags) + return + } + + args := p.args(u, runtime.GOOS) + totalTimeout := 60.0 + if len(p.Arguments) == 0 { + totalTimeout = float64(p.Count)*p.Timeout + float64(p.Count-1)*p.PingInterval + } + + out, err := p.pingHost(p.Binary, totalTimeout, args...) + if err != nil { + // Some implementations of ping return a 1 exit code on + // timeout, if this occurs we will not exit and try to parse + // the output. + status := -1 + if exitError, ok := err.(*exec.ExitError); ok { + if ws, ok := exitError.Sys().(syscall.WaitStatus); ok { + status = ws.ExitStatus() + } + } + + if status != 1 { + // Combine go err + stderr output + out = strings.TrimSpace(out) + if len(out) > 0 { + acc.AddError(fmt.Errorf("host %s: %s, %s", u, out, err)) + } else { + acc.AddError(fmt.Errorf("host %s: %s", u, err)) + } + fields["result_code"] = 2 + acc.AddFields("ping", fields, tags) + return + } + } + + trans, rec, min, avg, max, stddev, err := processPingOutput(out) + if err != nil { + // fatal error + acc.AddError(fmt.Errorf("%s: %s", err, u)) + fields["result_code"] = 2 + acc.AddFields("ping", fields, tags) + return + } + // Calculate packet loss percentage + loss := float64(trans-rec) / float64(trans) * 100.0 + fields["packets_transmitted"] = trans + fields["packets_received"] = rec + fields["percent_packet_loss"] = loss + if min >= 0 { + fields["minimum_response_ms"] = min + } + if avg >= 0 { + fields["average_response_ms"] = avg + } + if max >= 0 { + fields["maximum_response_ms"] = max + } + if stddev >= 0 { + fields["standard_deviation_ms"] = stddev + } + acc.AddFields("ping", fields, tags) +} + +func hostPinger(binary string, timeout float64, args ...string) (string, error) { + bin, err := exec.LookPath(binary) if err != nil { return "", err } @@ -173,15 +191,21 @@ func hostPinger(timeout float64, args ...string) (string, error) { // args returns the arguments for the 'ping' executable func (p *Ping) args(url string, system string) []string { - // Build the ping command args based on toml config + if len(p.Arguments) > 0 { + return p.Arguments + } + + // build the ping command args based on toml config args := []string{"-c", strconv.Itoa(p.Count), "-n", "-s", "16"} if p.PingInterval > 0 { args = append(args, "-i", strconv.FormatFloat(p.PingInterval, 'f', -1, 64)) } if p.Timeout > 0 { switch system { - case "darwin", "freebsd", "netbsd", "openbsd": + case "darwin": args = append(args, "-W", strconv.FormatFloat(p.Timeout*1000, 'f', -1, 64)) + case "freebsd", "netbsd", "openbsd": + args = append(args, "-w", strconv.FormatFloat(p.Timeout*1000, 'f', -1, 64)) case "linux": args = append(args, "-W", strconv.FormatFloat(p.Timeout, 'f', -1, 64)) default: @@ -196,19 +220,21 @@ func (p *Ping) args(url string, system string) []string { case "linux": args = append(args, "-w", strconv.Itoa(p.Deadline)) default: - // Not sure the best option here, just assume GNU ping? + // not sure the best option here, just assume gnu ping? args = append(args, "-w", strconv.Itoa(p.Deadline)) } } if p.Interface != "" { switch system { - case "darwin", "freebsd", "netbsd", "openbsd": - args = append(args, "-S", p.Interface) + case "darwin": + args = append(args, "-I", p.Interface) + case "freebsd", "netbsd", "openbsd": + args = append(args, "-s", p.Interface) case "linux": args = append(args, "-I", p.Interface) default: - // Not sure the best option here, just assume GNU ping? - args = append(args, "-I", p.Interface) + // not sure the best option here, just assume gnu ping? + args = append(args, "-i", p.Interface) } } args = append(args, url) @@ -217,7 +243,7 @@ func (p *Ping) args(url string, system string) []string { // processPingOutput takes in a string output from the ping command, like: // -// PING www.google.com (173.194.115.84): 56 data bytes +// ping www.google.com (173.194.115.84): 56 data bytes // 64 bytes from 173.194.115.84: icmp_seq=0 ttl=54 time=52.172 ms // 64 bytes from 173.194.115.84: icmp_seq=1 ttl=54 time=34.843 ms // @@ -280,6 +306,8 @@ func init() { Count: 1, Timeout: 1.0, Deadline: 10, + Binary: "ping", + Arguments: []string{}, } }) } diff --git a/plugins/inputs/ping/ping_test.go b/plugins/inputs/ping/ping_test.go index d5b82608a..867220b20 100644 --- a/plugins/inputs/ping/ping_test.go +++ b/plugins/inputs/ping/ping_test.go @@ -110,9 +110,9 @@ func TestArgs(t *testing.T) { system string output []string }{ - {"darwin", []string{"-c", "2", "-n", "-s", "16", "-i", "1.2", "-W", "12000", "-t", "24", "-S", "eth0", "www.google.com"}}, + {"darwin", []string{"-c", "2", "-n", "-s", "16", "-i", "1.2", "-W", "12000", "-t", "24", "-I", "eth0", "www.google.com"}}, {"linux", []string{"-c", "2", "-n", "-s", "16", "-i", "1.2", "-W", "12", "-w", "24", "-I", "eth0", "www.google.com"}}, - {"anything else", []string{"-c", "2", "-n", "-s", "16", "-i", "1.2", "-W", "12", "-w", "24", "-I", "eth0", "www.google.com"}}, + {"anything else", []string{"-c", "2", "-n", "-s", "16", "-i", "1.2", "-W", "12", "-w", "24", "-i", "eth0", "www.google.com"}}, } for i := range systemCases { actual := p.args("www.google.com", systemCases[i].system) @@ -124,7 +124,24 @@ func TestArgs(t *testing.T) { } } -func mockHostPinger(timeout float64, args ...string) (string, error) { +func TestArguments(t *testing.T) { + arguments := []string{"-c", "3"} + p := Ping{ + Count: 2, + Interface: "eth0", + Timeout: 12.0, + Deadline: 24, + PingInterval: 1.2, + Arguments: arguments, + } + + for _, system := range []string{"darwin", "linux", "anything else"} { + actual := p.args("www.google.com", system) + require.True(t, reflect.DeepEqual(actual, arguments), "Expected: %s Actual: %s", arguments, actual) + } +} + +func mockHostPinger(binary string, timeout float64, args ...string) (string, error) { return linuxPingOutput, nil } @@ -165,7 +182,7 @@ PING www.google.com (216.58.218.164) 56(84) bytes of data. rtt min/avg/max/mdev = 35.225/44.033/51.806/5.325 ms ` -func mockLossyHostPinger(timeout float64, args ...string) (string, error) { +func mockLossyHostPinger(binary string, timeout float64, args ...string) (string, error) { return lossyPingOutput, nil } @@ -200,7 +217,7 @@ Request timeout for icmp_seq 0 2 packets transmitted, 0 packets received, 100.0% packet loss ` -func mockErrorHostPinger(timeout float64, args ...string) (string, error) { +func mockErrorHostPinger(binary string, timeout float64, args ...string) (string, error) { // This error will not trigger correct error paths return errorPingOutput, nil } @@ -225,7 +242,7 @@ func TestBadPingGather(t *testing.T) { acc.AssertContainsTaggedFields(t, "ping", fields, tags) } -func mockFatalHostPinger(timeout float64, args ...string) (string, error) { +func mockFatalHostPinger(binary string, timeout float64, args ...string) (string, error) { return fatalPingOutput, errors.New("So very bad") } @@ -265,7 +282,7 @@ func TestErrorWithHostNamePingGather(t *testing.T) { var acc testutil.Accumulator p := Ping{ Urls: []string{"www.amazon.com"}, - pingHost: func(timeout float64, args ...string) (string, error) { + pingHost: func(binary string, timeout float64, args ...string) (string, error) { return param.out, errors.New("So very bad") }, } @@ -274,3 +291,16 @@ func TestErrorWithHostNamePingGather(t *testing.T) { assert.Contains(t, acc.Errors, param.error) } } + +func TestPingBinary(t *testing.T) { + var acc testutil.Accumulator + p := Ping{ + Urls: []string{"www.google.com"}, + Binary: "ping6", + pingHost: func(binary string, timeout float64, args ...string) (string, error) { + assert.True(t, binary == "ping6") + return "", nil + }, + } + acc.GatherError(p.Gather) +} diff --git a/plugins/inputs/ping/ping_windows.go b/plugins/inputs/ping/ping_windows.go index 06a7f590e..6064fabe4 100644 --- a/plugins/inputs/ping/ping_windows.go +++ b/plugins/inputs/ping/ping_windows.go @@ -4,6 +4,7 @@ package ping import ( "errors" + "fmt" "net" "os/exec" "regexp" @@ -20,9 +21,11 @@ import ( // HostPinger is a function that runs the "ping" function using a list of // passed arguments. This can be easily switched with a mocked ping function // for unit test purposes (see ping_test.go) -type HostPinger func(timeout float64, args ...string) (string, error) +type HostPinger func(binary string, timeout float64, args ...string) (string, error) type Ping struct { + wg sync.WaitGroup + // Number of pings to send (ping -c ) Count int @@ -32,6 +35,13 @@ type Ping struct { // URLs to ping Urls []string + // Ping executable binary + Binary string + + // Arguments for ping command. + // when `Arguments` is not empty, other options (ping_interval, timeout, etc) will be ignored + Arguments []string + // host ping function pingHost HostPinger } @@ -49,14 +59,100 @@ const sampleConfig = ` ## Ping timeout, in seconds. 0.0 means default timeout (ping -w ) # timeout = 0.0 + + ## Specify the ping executable binary, default is "ping" + # binary = "ping" + + ## Arguments for ping command + ## when arguments is not empty, other options (ping_interval, timeout, etc) will be ignored + # arguments = ["-c", "3"] ` func (s *Ping) SampleConfig() string { return sampleConfig } -func hostPinger(timeout float64, args ...string) (string, error) { - bin, err := exec.LookPath("ping") +func (p *Ping) Gather(acc telegraf.Accumulator) error { + if p.Count < 1 { + p.Count = 1 + } + + // Spin off a go routine for each url to ping + for _, url := range p.Urls { + p.wg.Add(1) + go p.pingToURL(url, acc) + } + + p.wg.Wait() + + return nil +} + +func (p *Ping) pingToURL(u string, acc telegraf.Accumulator) { + defer p.wg.Done() + + tags := map[string]string{"url": u} + fields := map[string]interface{}{"result_code": 0} + + _, err := net.LookupHost(u) + if err != nil { + acc.AddError(err) + fields["result_code"] = 1 + acc.AddFields("ping", fields, tags) + return + } + + args := p.args(u) + totalTimeout := 60.0 + if len(p.Arguments) == 0 { + totalTimeout = p.timeout() * float64(p.Count) + } + + out, err := p.pingHost(p.Binary, totalTimeout, args...) + // ping host return exitcode != 0 also when there was no response from host + // but command was execute successfully + var pendingError error + if err != nil { + // Combine go err + stderr output + pendingError = errors.New(strings.TrimSpace(out) + ", " + err.Error()) + } + trans, recReply, receivePacket, avg, min, max, err := processPingOutput(out) + if err != nil { + // fatal error + if pendingError != nil { + acc.AddError(fmt.Errorf("%s: %s", pendingError, u)) + } else { + acc.AddError(fmt.Errorf("%s: %s", err, u)) + } + + fields["result_code"] = 2 + fields["errors"] = 100.0 + acc.AddFields("ping", fields, tags) + return + } + // Calculate packet loss percentage + lossReply := float64(trans-recReply) / float64(trans) * 100.0 + lossPackets := float64(trans-receivePacket) / float64(trans) * 100.0 + + fields["packets_transmitted"] = trans + fields["reply_received"] = recReply + fields["packets_received"] = receivePacket + fields["percent_packet_loss"] = lossPackets + fields["percent_reply_loss"] = lossReply + if avg >= 0 { + fields["average_response_ms"] = float64(avg) + } + if min >= 0 { + fields["minimum_response_ms"] = float64(min) + } + if max >= 0 { + fields["maximum_response_ms"] = float64(max) + } + acc.AddFields("ping", fields, tags) +} + +func hostPinger(binary string, timeout float64, args ...string) (string, error) { + bin, err := exec.LookPath(binary) if err != nil { return "", err } @@ -66,6 +162,23 @@ func hostPinger(timeout float64, args ...string) (string, error) { return string(out), err } +// args returns the arguments for the 'ping' executable +func (p *Ping) args(url string) []string { + if len(p.Arguments) > 0 { + return p.Arguments + } + + args := []string{"-n", strconv.Itoa(p.Count)} + + if p.Timeout > 0 { + args = append(args, "-w", strconv.FormatFloat(p.Timeout*1000, 'f', 0, 64)) + } + + args = append(args, url) + + return args +} + // processPingOutput takes in a string output from the ping command // based on linux implementation but using regex ( multilanguage support ) // It returns (, , , , , ) @@ -134,106 +247,13 @@ func (p *Ping) timeout() float64 { return 4 + 1 } -// args returns the arguments for the 'ping' executable -func (p *Ping) args(url string) []string { - args := []string{"-n", strconv.Itoa(p.Count)} - - if p.Timeout > 0 { - args = append(args, "-w", strconv.FormatFloat(p.Timeout*1000, 'f', 0, 64)) - } - - args = append(args, url) - - return args -} - -func (p *Ping) Gather(acc telegraf.Accumulator) error { - if p.Count < 1 { - p.Count = 1 - } - var wg sync.WaitGroup - errorChannel := make(chan error, len(p.Urls)*2) - var pendingError error = nil - // Spin off a go routine for each url to ping - for _, url := range p.Urls { - wg.Add(1) - go func(u string) { - defer wg.Done() - - tags := map[string]string{"url": u} - fields := map[string]interface{}{"result_code": 0} - - _, err := net.LookupHost(u) - if err != nil { - errorChannel <- err - fields["result_code"] = 1 - acc.AddFields("ping", fields, tags) - return - } - - args := p.args(u) - totalTimeout := p.timeout() * float64(p.Count) - out, err := p.pingHost(totalTimeout, args...) - // ping host return exitcode != 0 also when there was no response from host - // but command was execute successfully - if err != nil { - // Combine go err + stderr output - pendingError = errors.New(strings.TrimSpace(out) + ", " + err.Error()) - } - trans, recReply, receivePacket, avg, min, max, err := processPingOutput(out) - if err != nil { - // fatal error - if pendingError != nil { - errorChannel <- pendingError - } - errorChannel <- err - - fields["errors"] = 100.0 - acc.AddFields("ping", fields, tags) - return - } - // Calculate packet loss percentage - lossReply := float64(trans-recReply) / float64(trans) * 100.0 - lossPackets := float64(trans-receivePacket) / float64(trans) * 100.0 - - fields["packets_transmitted"] = trans - fields["reply_received"] = recReply - fields["packets_received"] = receivePacket - fields["percent_packet_loss"] = lossPackets - fields["percent_reply_loss"] = lossReply - if avg >= 0 { - fields["average_response_ms"] = float64(avg) - } - if min >= 0 { - fields["minimum_response_ms"] = float64(min) - } - if max >= 0 { - fields["maximum_response_ms"] = float64(max) - } - acc.AddFields("ping", fields, tags) - }(url) - } - - wg.Wait() - close(errorChannel) - - // Get all errors and return them as one giant error - errorStrings := []string{} - for err := range errorChannel { - errorStrings = append(errorStrings, err.Error()) - } - - if len(errorStrings) == 0 { - return nil - } - return errors.New(strings.Join(errorStrings, "\n")) -} - func init() { inputs.Add("ping", func() telegraf.Input { return &Ping{ - pingHost: hostPinger, - Count: 1, + pingHost: hostPinger, + Count: 1, + Binary: "ping", + Arguments: []string{}, } }) } diff --git a/plugins/inputs/ping/ping_windows_test.go b/plugins/inputs/ping/ping_windows_test.go index 178e42fcb..4618ec4db 100644 --- a/plugins/inputs/ping/ping_windows_test.go +++ b/plugins/inputs/ping/ping_windows_test.go @@ -4,10 +4,12 @@ package ping import ( "errors" + "reflect" "testing" "github.com/influxdata/telegraf/testutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Windows ping format ( should support multilanguage ?) @@ -59,7 +61,7 @@ func TestHost(t *testing.T) { assert.Equal(t, 52, max, "Max 52") } -func mockHostPinger(timeout float64, args ...string) (string, error) { +func mockHostPinger(binary string, timeout float64, args ...string) (string, error) { return winENPingOutput, nil } @@ -102,7 +104,7 @@ Statystyka badania ping dla 195.187.242.157: (100% straty), ` -func mockErrorHostPinger(timeout float64, args ...string) (string, error) { +func mockErrorHostPinger(binary string, timeout float64, args ...string) (string, error) { return errorPingOutput, errors.New("No packets received") } @@ -128,6 +130,18 @@ func TestBadPingGather(t *testing.T) { acc.AssertContainsTaggedFields(t, "ping", fields, tags) } +func TestArguments(t *testing.T) { + arguments := []string{"-c", "3"} + p := Ping{ + Count: 2, + Timeout: 12.0, + Arguments: arguments, + } + + actual := p.args("www.google.com") + require.True(t, reflect.DeepEqual(actual, arguments), "Expected : %s Actual: %s", arguments, actual) +} + var lossyPingOutput = ` Badanie thecodinglove.com [66.6.44.4] z 9800 bajtami danych: Upłynął limit czasu żądania. @@ -147,7 +161,7 @@ Szacunkowy czas błądzenia pakietów w millisekundach: Minimum = 114 ms, Maksimum = 119 ms, Czas średni = 115 ms ` -func mockLossyHostPinger(timeout float64, args ...string) (string, error) { +func mockLossyHostPinger(binary string, timeout float64, args ...string) (string, error) { return lossyPingOutput, nil } @@ -207,7 +221,7 @@ Options: ` -func mockFatalHostPinger(timeout float64, args ...string) (string, error) { +func mockFatalHostPinger(binary string, timeout float64, args ...string) (string, error) { return fatalPingOutput, errors.New("So very bad") } @@ -249,7 +263,7 @@ Ping statistics for 8.8.8.8: Packets: Sent = 4, Received = 1, Lost = 3 (75% loss), ` -func mockUnreachableHostPinger(timeout float64, args ...string) (string, error) { +func mockUnreachableHostPinger(binary string, timeout float64, args ...string) (string, error) { return UnreachablePingOutput, errors.New("So very bad") } @@ -298,7 +312,7 @@ Ping statistics for 8.8.8.8: Packets: Sent = 4, Received = 1, Lost = 3 (75% loss), ` -func mockTTLExpiredPinger(timeout float64, args ...string) (string, error) { +func mockTTLExpiredPinger(binary string, timeout float64, args ...string) (string, error) { return TTLExpiredPingOutput, errors.New("So very bad") } @@ -333,3 +347,16 @@ func TestTTLExpiredPingGather(t *testing.T) { assert.False(t, acc.HasInt64Field("ping", "minimum_response_ms"), "Fatal ping should not have packet measurements") } + +func TestPingBinary(t *testing.T) { + var acc testutil.Accumulator + p := Ping{ + Urls: []string{"www.google.com"}, + Binary: "ping6", + pingHost: func(binary string, timeout float64, args ...string) (string, error) { + assert.True(t, binary == "ping6") + return "", nil + }, + } + acc.GatherError(p.Gather) +}