You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
308 lines
11 KiB
308 lines
11 KiB
|
2 months ago
|
package diagnostics
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"net"
|
||
|
|
"os"
|
||
|
|
"reflect"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type fakeExecutor struct {
|
||
|
|
path string
|
||
|
|
args []string
|
||
|
|
maxOutput int
|
||
|
|
result Execution
|
||
|
|
err error
|
||
|
|
}
|
||
|
|
|
||
|
|
type waitingExecutor struct{}
|
||
|
|
|
||
|
|
type executorCall struct {
|
||
|
|
path string
|
||
|
|
args []string
|
||
|
|
}
|
||
|
|
|
||
|
|
type sequenceExecutor struct {
|
||
|
|
calls []executorCall
|
||
|
|
results []Execution
|
||
|
|
index int
|
||
|
|
}
|
||
|
|
|
||
|
|
func (e *sequenceExecutor) Execute(_ context.Context, path string, args []string, _ int) (Execution, error) {
|
||
|
|
e.calls = append(e.calls, executorCall{path: path, args: append([]string(nil), args...)})
|
||
|
|
if e.index >= len(e.results) {
|
||
|
|
return Execution{}, errors.New("unexpected diagnostic execution")
|
||
|
|
}
|
||
|
|
result := e.results[e.index]
|
||
|
|
e.index++
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (waitingExecutor) Execute(ctx context.Context, _ string, _ []string, _ int) (Execution, error) {
|
||
|
|
<-ctx.Done()
|
||
|
|
return Execution{}, ctx.Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (e *fakeExecutor) Execute(_ context.Context, path string, args []string, maxOutput int) (Execution, error) {
|
||
|
|
e.path = path
|
||
|
|
e.args = append([]string(nil), args...)
|
||
|
|
e.maxOutput = maxOutput
|
||
|
|
return e.result, e.err
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestProcessRunnerUsesFixedExecutableAndArguments(t *testing.T) {
|
||
|
|
executor := &fakeExecutor{result: Execution{Output: "ok", Bytes: 2, ExitCode: 0}}
|
||
|
|
runner, err := NewProcessRunner(ProcessRunnerOptions{
|
||
|
|
PingPath: "/opt/tera/bin/ping",
|
||
|
|
TraceroutePath: "/opt/tera/bin/traceroute",
|
||
|
|
Executor: executor,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
result, err := runner.Run(context.Background(), Request{Kind: KindNetworkTest, Target: "192.0.2.1", TimeoutSeconds: 30, MaxOutputBytes: 4096})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Run() error = %v", err)
|
||
|
|
}
|
||
|
|
if executor.path != "/opt/tera/bin/ping" || !reflect.DeepEqual(executor.args, []string{"-c", "4", "-s", "56", "-W", "1", "-w", "5", "192.0.2.1"}) {
|
||
|
|
t.Fatalf("command = %q %#v", executor.path, executor.args)
|
||
|
|
}
|
||
|
|
if executor.maxOutput != 4096 || result.Reachable == nil || !*result.Reachable {
|
||
|
|
t.Fatalf("execution limit/result = %d %+v", executor.maxOutput, result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestProcessRunnerUsesTTLIncrementingPingTraceroute(t *testing.T) {
|
||
|
|
executor := &sequenceExecutor{results: []Execution{
|
||
|
|
{Output: "From 192.0.2.1 icmp_seq=1 Time to live exceeded", ExitCode: 1},
|
||
|
|
{Output: "64 bytes from 192.0.2.10: icmp_seq=1 ttl=63 time=1.25 ms", ExitCode: 0},
|
||
|
|
}}
|
||
|
|
runner, err := NewProcessRunner(ProcessRunnerOptions{
|
||
|
|
PingPath: "/opt/tera/bin/ping",
|
||
|
|
TraceroutePath: "/opt/tera/bin/traceroute",
|
||
|
|
Executor: executor,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
result, err := runner.Run(context.Background(), Request{Kind: KindTraceroute, Target: "192.0.2.10", MaxHops: 24, TimeoutSeconds: 77, MaxOutputBytes: 4096})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Run() error = %v", err)
|
||
|
|
}
|
||
|
|
wantCalls := []executorCall{
|
||
|
|
{path: "/opt/tera/bin/ping", args: []string{"-c", "1", "-s", "56", "-W", "1", "-t", "1", "192.0.2.10"}},
|
||
|
|
{path: "/opt/tera/bin/ping", args: []string{"-c", "1", "-s", "56", "-W", "1", "-t", "2", "192.0.2.10"}},
|
||
|
|
}
|
||
|
|
if !reflect.DeepEqual(executor.calls, wantCalls) {
|
||
|
|
t.Fatalf("commands = %#v", executor.calls)
|
||
|
|
}
|
||
|
|
if result.Reachable == nil || !*result.Reachable || result.MaxHops != 24 || result.ExitCode != 0 {
|
||
|
|
t.Fatalf("result = %+v, want reachable traceroute", result)
|
||
|
|
}
|
||
|
|
if !strings.Contains(result.Output, " 1 192.0.2.1") || !strings.Contains(result.Output, " 2 192.0.2.10 1.25 ms") {
|
||
|
|
t.Fatalf("output = %q", result.Output)
|
||
|
|
}
|
||
|
|
if operationResult := result.OperationResult(77, 4096); operationResult["max_hops"] != 24 {
|
||
|
|
t.Fatalf("operation result = %#v, want max_hops 24", operationResult)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestProcessRunnerPingTracerouteStopsOnHostUnreachable(t *testing.T) {
|
||
|
|
executor := &sequenceExecutor{results: []Execution{
|
||
|
|
{Output: "From 192.0.2.1 icmp_seq=1 Destination Host Unreachable", ExitCode: 1},
|
||
|
|
}}
|
||
|
|
runner, err := NewProcessRunner(ProcessRunnerOptions{PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: executor})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
result, err := runner.Run(context.Background(), Request{Kind: KindTraceroute, Target: "192.0.2.10", MaxHops: 10, TimeoutSeconds: 35, MaxOutputBytes: 1024})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if len(executor.calls) != 1 || result.Reachable == nil || *result.Reachable || result.ExitCode != 1 || !strings.Contains(result.Output, "!H") {
|
||
|
|
t.Fatalf("calls/result = %#v %+v", executor.calls, result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestProcessRunnerTreatsDiagnosticExitAsResult(t *testing.T) {
|
||
|
|
executor := &fakeExecutor{result: Execution{Output: "unreachable", Bytes: 11, ExitCode: 1}}
|
||
|
|
runner, err := NewProcessRunner(ProcessRunnerOptions{PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: executor})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
result, err := runner.Run(context.Background(), Request{Kind: KindNetworkTest, Target: "example.test", TimeoutSeconds: 5, MaxOutputBytes: 1024})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Run() error = %v", err)
|
||
|
|
}
|
||
|
|
if result.ExitCode != 1 || result.Reachable == nil || *result.Reachable {
|
||
|
|
t.Fatalf("result = %+v, want unreachable exit 1", result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestProcessRunnerAppliesRequestedPingCount(t *testing.T) {
|
||
|
|
executor := &fakeExecutor{result: Execution{Output: "ok", Bytes: 2, ExitCode: 0}}
|
||
|
|
runner, err := NewProcessRunner(ProcessRunnerOptions{
|
||
|
|
PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: executor,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
result, err := runner.Run(context.Background(), Request{
|
||
|
|
Kind: KindNetworkTest, Target: "192.0.2.1", Count: 5, TimeoutSeconds: 7, MaxOutputBytes: 1024,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
wantArgs := []string{"-c", "5", "-s", "56", "-W", "1", "-w", "6", "192.0.2.1"}
|
||
|
|
if !reflect.DeepEqual(executor.args, wantArgs) || result.Count != 5 {
|
||
|
|
t.Fatalf("args/result = %#v %+v, want count 5", executor.args, result)
|
||
|
|
}
|
||
|
|
if operationResult := result.OperationResult(7, 1024); operationResult["count"] != 5 {
|
||
|
|
t.Fatalf("operation result = %#v, want count 5", operationResult)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateRequestRejectsInvalidPingCount(t *testing.T) {
|
||
|
|
for _, count := range []int{-1, 21} {
|
||
|
|
request := Request{Kind: KindNetworkTest, Target: "192.0.2.1", Count: count, TimeoutSeconds: 5, MaxOutputBytes: 1024}
|
||
|
|
if err := ValidateRequest(request); err == nil {
|
||
|
|
t.Fatalf("ValidateRequest() accepted ping count %d", count)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateRequestRejectsInvalidTracerouteMaxHops(t *testing.T) {
|
||
|
|
for _, maxHops := range []int{-1, MaximumTracerouteMaxHops + 1} {
|
||
|
|
request := Request{Kind: KindTraceroute, Target: "192.0.2.1", MaxHops: maxHops, TimeoutSeconds: 30, MaxOutputBytes: 1024}
|
||
|
|
if err := ValidateRequest(request); err == nil {
|
||
|
|
t.Fatalf("ValidateRequest() accepted traceroute max hops %d", maxHops)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestProcessRunnerEnforcesRequestTimeout(t *testing.T) {
|
||
|
|
runner, err := NewProcessRunner(ProcessRunnerOptions{
|
||
|
|
PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: waitingExecutor{},
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
started := time.Now()
|
||
|
|
_, err = runner.Run(context.Background(), Request{Kind: KindNetworkTest, Target: "192.0.2.1", TimeoutSeconds: 1, MaxOutputBytes: 1024})
|
||
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
||
|
|
t.Fatalf("Run() error = %v, want deadline exceeded", err)
|
||
|
|
}
|
||
|
|
if elapsed := time.Since(started); elapsed > 2*time.Second {
|
||
|
|
t.Fatalf("Run() timeout took %s", elapsed)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateRequestRejectsOptionAndShellLikeTargets(t *testing.T) {
|
||
|
|
for _, target := range []string{"-c", "host;reboot", "$(id)", "host name"} {
|
||
|
|
request := Request{Kind: KindNetworkTest, Target: target, TimeoutSeconds: 5, MaxOutputBytes: 1024}
|
||
|
|
if err := ValidateRequest(request); err == nil {
|
||
|
|
t.Fatalf("ValidateRequest() accepted %q", target)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestProcessRunnerTCPConnectionTestConnectsToRequestedPort(t *testing.T) {
|
||
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer listener.Close()
|
||
|
|
|
||
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
||
|
|
runner, err := NewProcessRunner(ProcessRunnerOptions{PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
result, err := runner.Run(context.Background(), Request{
|
||
|
|
Kind: KindTCPConnectionTest, Target: "127.0.0.1", Port: port, TimeoutSeconds: 2, MaxOutputBytes: 1024,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Run() error = %v", err)
|
||
|
|
}
|
||
|
|
if result.Port != port || result.Reachable == nil || !*result.Reachable || result.ExitCode != 0 || result.OutputTruncated {
|
||
|
|
t.Fatalf("result = %+v", result)
|
||
|
|
}
|
||
|
|
operationResult := result.OperationResult(2, 1024)
|
||
|
|
if operationResult["adapter"] != "socket" || operationResult["port"] != port {
|
||
|
|
t.Fatalf("operation result = %#v", operationResult)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateRequestRequiresTCPPort(t *testing.T) {
|
||
|
|
for _, port := range []int{0, 65536} {
|
||
|
|
request := Request{Kind: KindTCPConnectionTest, Target: "127.0.0.1", Port: port, TimeoutSeconds: 5, MaxOutputBytes: 1024}
|
||
|
|
if err := ValidateRequest(request); err == nil {
|
||
|
|
t.Fatalf("ValidateRequest() accepted TCP port %d", port)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOSExecutorBoundsCombinedOutputAndReturnsExitCode(t *testing.T) {
|
||
|
|
if os.Getenv("TERA_DIAGNOSTICS_HELPER") == "output" {
|
||
|
|
fmt.Print(strings.Repeat("x", 4096))
|
||
|
|
os.Exit(3)
|
||
|
|
}
|
||
|
|
t.Setenv("TERA_DIAGNOSTICS_HELPER", "output")
|
||
|
|
execution, err := (OSExecutor{}).Execute(context.Background(), os.Args[0], []string{"-test.run=TestOSExecutorBoundsCombinedOutputAndReturnsExitCode"}, 1024)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Execute() error = %v", err)
|
||
|
|
}
|
||
|
|
if execution.ExitCode != 3 || execution.Bytes != 1024 || len(execution.Output) != 1024 || !execution.Truncated {
|
||
|
|
t.Fatalf("execution = %+v", execution)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOSExecutorReportsLiveOutput(t *testing.T) {
|
||
|
|
if os.Getenv("TERA_DIAGNOSTICS_HELPER") == "stream" {
|
||
|
|
fmt.Print("first reply\n")
|
||
|
|
time.Sleep(50 * time.Millisecond)
|
||
|
|
fmt.Print("second reply\n")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
t.Setenv("TERA_DIAGNOSTICS_HELPER", "stream")
|
||
|
|
var mu sync.Mutex
|
||
|
|
reports := make([]OutputProgress, 0, 2)
|
||
|
|
ctx := WithOutputReporter(context.Background(), func(progress OutputProgress) {
|
||
|
|
mu.Lock()
|
||
|
|
defer mu.Unlock()
|
||
|
|
reports = append(reports, progress)
|
||
|
|
})
|
||
|
|
execution, err := (OSExecutor{}).Execute(ctx, os.Args[0], []string{"-test.run=TestOSExecutorReportsLiveOutput"}, 1024)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
mu.Lock()
|
||
|
|
defer mu.Unlock()
|
||
|
|
if len(reports) == 0 || !strings.Contains(reports[len(reports)-1].Output, "first reply") || !strings.Contains(reports[len(reports)-1].Output, "second reply") {
|
||
|
|
t.Fatalf("live reports = %#v", reports)
|
||
|
|
}
|
||
|
|
if execution.Output != reports[len(reports)-1].Output {
|
||
|
|
t.Fatalf("final output = %q, last live output = %q", execution.Output, reports[len(reports)-1].Output)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOSExecutorHonorsContextTimeout(t *testing.T) {
|
||
|
|
if os.Getenv("TERA_DIAGNOSTICS_HELPER") == "sleep" {
|
||
|
|
time.Sleep(5 * time.Second)
|
||
|
|
os.Exit(0)
|
||
|
|
}
|
||
|
|
t.Setenv("TERA_DIAGNOSTICS_HELPER", "sleep")
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||
|
|
defer cancel()
|
||
|
|
_, err := (OSExecutor{}).Execute(ctx, os.Args[0], []string{"-test.run=TestOSExecutorHonorsContextTimeout"}, 1024)
|
||
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
||
|
|
t.Fatalf("Execute() error = %v, want deadline exceeded", err)
|
||
|
|
}
|
||
|
|
}
|