diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 8c8d711..aee5b40 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,7 +2,20 @@ + + + + + + + + + + + + + diff --git a/README.md b/README.md index 41d8d92..b5cf90b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,81 @@ # go-iperf A Go based wrapper around iperf3 +## Basic Usage +basic client setup +```go +func main() { + + c := iperf.NewClient("192.168.0.10") + c.SetJSON(true) + c.SetIncludeServer(true) + c.SetStreams(4) + c.SetTimeSec(30) + c.SetInterval(1) + + err := c.Start() + if err != nil { + fmt.Printf("failed to start client: %v\n", err) + os.Exit(-1) + } + + <- c.Done + + fmt.Println(c.Report().String()) +} +``` + +basic server setup +```go +func main() { + + s := iperf.NewServer() + err := s.Start() + if err != nil { + fmt.Printf("failed to start server: %v\n", err) + os.Exit(-1) + } + + for s.Running() { + time.Sleep(100 * time.Millisecond) + } + + fmt.Println("server finished") +} +``` + +client with live results printing +```go +func main() { + + c := iperf.NewClient("192.168.0.10") + c.SetJSON(true) + c.SetIncludeServer(true) + c.SetStreams(4) + c.SetTimeSec(30) + c.SetInterval(1) + liveReports := c.SetModeLive() + + go func() { + for report := range liveReports { + fmt.Println(report.String()) + } + } + + err := c.Start() + if err != nil { + fmt.Printf("failed to start client: %v\n", err) + os.Exit(-1) + } + + <- c.Done + + fmt.Println(c.Report().String()) +} +``` + +building binary data package with iperf binaries +``` +go-bindata -pkg iperf -prefix "embedded/" embedded/ ``` -go-bindata -pkg iperf -prefix "embedded/" embedded/``` diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..577dd7d --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,21 @@ +# Dockerfile.protogen +FROM golang:latest + +LABEL maintainer="Benjamin Grewell " + +ENV PROTOC_VERSION 3.6.1 +ENV PROTOC_GEN_GO_VERSION v1.2.0 + +WORKDIR /go/src/github.com/BGrewell/go-iperf/api/proto + +RUN apt update +RUN apt install -y protobuf-compiler python3 python3-pip +RUN go get -u github.com/golang/protobuf/protoc-gen-go +RUN pip3 install grpcio-tools +RUN export PATH=$PATH:$GOPATH/bin +RUN echo $PATH + +COPY proto/control.proto control.proto + +RUN mkdir go +RUN protoc -I /. --go_out=plugins=grpc:go control.proto diff --git a/api/build.sh b/api/build.sh new file mode 100644 index 0000000..bd895f7 --- /dev/null +++ b/api/build.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +if [ ! -d go ]; then + echo "[!] Creating go output directory" + mkdir go; +fi + +echo "[+] Building docker container" +docker image build -t go-iperf-builder:1.0 . +docker container run --detach --name builder go-iperf-builder:1.0 +docker cp grpc:/go/src/github.com/BGrewell/go-iperf/api/go/api.pb.go go/. +echo "[+] Updating of go library complete" + +echo "[+] Removing docker container" +docker rm builder + +echo "[+] Adding new files to source control" +git add go/api.pb.go +git commit -m "regenerated grpc libraries" +git push + +echo "[+] Done. Everything has been rebuilt and the repository has been updated and pushed" \ No newline at end of file diff --git a/api/proto/control.proto b/api/proto/control.proto new file mode 100644 index 0000000..ff997e3 --- /dev/null +++ b/api/proto/control.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; + +// [START java_declaration] +option java_multiple_files = true; +option java_package = "com.bengrewell.go-iperf.control"; +option java_outer_classname = "Control"; +// [END java_declaration] + +// [START csharp_declaration] +option csharp_namespace = "BenGrewell.GoIperf.Control"; +// [END csharp_declaration] + +package api; + +service Command { + rpc StartServer(StartServerRequest) returns (StartServerResponse) {} +} + +message StartServerRequest { +} + +message StartServerResponse { + string id = 1; + int32 listen_port = 2; +} \ No newline at end of file diff --git a/client.go b/client.go index e7e27b9..57c644f 100644 --- a/client.go +++ b/client.go @@ -62,9 +62,9 @@ type ClientOptions struct { } type Client struct { - Id string `json:"id" yaml:"id" xml:"id"` - Running bool `json:"running" yaml:"running" xml:"running"` - Done chan bool `json:"-" yaml:"-" xml:"-"` + Id string `json:"id" yaml:"id" xml:"id"` + Running bool `json:"running" yaml:"running" xml:"running"` + Done chan bool `json:"-" yaml:"-" xml:"-"` Options *ClientOptions `json:"options" yaml:"options" xml:"options"` exitCode *int report *TestReport @@ -487,7 +487,6 @@ func (c *Client) SetModeLive() <-chan *StreamIntervalReport { } func (c *Client) Start() (err error) { - //todo: Need to build the string based on the Options above that are set cmd, err := c.commandString() if err != nil { return err diff --git a/controller.go b/controller.go new file mode 100644 index 0000000..81ca294 --- /dev/null +++ b/controller.go @@ -0,0 +1,26 @@ +package iperf + +type Controller struct { + clients map[string]*Client + servers map[string]*Server +} + +func (c *Controller) StartListener() (err error) { + +} + +func (c *Controller) StopListener() (err error) { + +} + +func (c *Controller) StopServer(id string) (err error) { + +} + +func (c *Controller) NewServer() (server *Server, err error) { + +} + +func (c *Controller) NewClient(serverAddr string) (client *Client, err error) { + +} diff --git a/reporter.go b/reporter.go index fe6159d..c1d72c1 100644 --- a/reporter.go +++ b/reporter.go @@ -9,7 +9,7 @@ type Reporter struct { ReportingChannel chan *StreamIntervalReport LogFile string running bool - tailer *tail.Tail + tailer *tail.Tail } func (r *Reporter) Start() { diff --git a/reporter_linux.go b/reporter_linux.go index e36c5c4..40f0f31 100644 --- a/reporter_linux.go +++ b/reporter_linux.go @@ -59,70 +59,70 @@ func (r *Reporter) runLogProcessor() { for { select { - case line := <- r.tailer.Lines: - if line == nil { + case line := <-r.tailer.Lines: + if line == nil { + continue + } + if len(line.Text) > 5 { + id := line.Text[1:4] + stream, err := strconv.Atoi(strings.TrimSpace(id)) + if err != nil { continue } - if len(line.Text) > 5 { - id := line.Text[1:4] - stream, err := strconv.Atoi(strings.TrimSpace(id)) - if err != nil { + fields := strings.Fields(line.Text[5:]) + if len(fields) >= 9 { + if fields[0] == "local" { continue } - fields := strings.Fields(line.Text[5:]) - if len(fields) >= 9 { - if fields[0] == "local" { - continue - } - timeFields := strings.Split(fields[0], "-") - start, err := strconv.ParseFloat(timeFields[0], 32) - if err != nil { - log.Printf("failed to convert start time: %s\n", err) - } - end, err := strconv.ParseFloat(timeFields[1], 32) - transferedStr := fmt.Sprintf("%s%s", fields[2], fields[3]) - transferedBytes, err := conversions.StringBitRateToInt(transferedStr) - if err != nil { - log.Printf("failed to convert units: %s\n", err) - } - transferedBytes = transferedBytes / 8 - rateStr := fmt.Sprintf("%s%s", fields[4], fields[5]) - rate, err := conversions.StringBitRateToInt(rateStr) - if err != nil { - log.Printf("failed to convert units: %s\n", err) - } - retrans, err := strconv.Atoi(fields[6]) - if err != nil { - log.Printf("failed to convert units: %s\n", err) - } - cwndStr := fmt.Sprintf("%s%s", fields[7], fields[8]) - cwnd, err := conversions.StringBitRateToInt(cwndStr) - if err != nil { - log.Printf("failed to convert units: %s\n", err) - } - cwnd = cwnd / 8 - omitted := false - if len(fields) >= 10 && fields[9] == "(omitted)" { - omitted = true - } - report := &StreamIntervalReport{ - Socket: stream, - StartInterval: float32(start), - EndInterval: float32(end), - Seconds: float32(end - start), - Bytes: int(transferedBytes), - BitsPerSecond: float64(rate), - Retransmissions: retrans, - CongestionWindow: int(cwnd), - Omitted: omitted, - } - r.ReportingChannel <- report + timeFields := strings.Split(fields[0], "-") + start, err := strconv.ParseFloat(timeFields[0], 32) + if err != nil { + log.Printf("failed to convert start time: %s\n", err) } + end, err := strconv.ParseFloat(timeFields[1], 32) + transferedStr := fmt.Sprintf("%s%s", fields[2], fields[3]) + transferedBytes, err := conversions.StringBitRateToInt(transferedStr) + if err != nil { + log.Printf("failed to convert units: %s\n", err) + } + transferedBytes = transferedBytes / 8 + rateStr := fmt.Sprintf("%s%s", fields[4], fields[5]) + rate, err := conversions.StringBitRateToInt(rateStr) + if err != nil { + log.Printf("failed to convert units: %s\n", err) + } + retrans, err := strconv.Atoi(fields[6]) + if err != nil { + log.Printf("failed to convert units: %s\n", err) + } + cwndStr := fmt.Sprintf("%s%s", fields[7], fields[8]) + cwnd, err := conversions.StringBitRateToInt(cwndStr) + if err != nil { + log.Printf("failed to convert units: %s\n", err) + } + cwnd = cwnd / 8 + omitted := false + if len(fields) >= 10 && fields[9] == "(omitted)" { + omitted = true + } + report := &StreamIntervalReport{ + Socket: stream, + StartInterval: float32(start), + EndInterval: float32(end), + Seconds: float32(end - start), + Bytes: int(transferedBytes), + BitsPerSecond: float64(rate), + Retransmissions: retrans, + CongestionWindow: int(cwnd), + Omitted: omitted, + } + r.ReportingChannel <- report } - case <- time.After(100 * time.Millisecond): - if !r.running { - return - } + } + case <-time.After(100 * time.Millisecond): + if !r.running { + return + } } } } diff --git a/reporter_windows.go b/reporter_windows.go index 684e776..f714854 100644 --- a/reporter_windows.go +++ b/reporter_windows.go @@ -24,7 +24,7 @@ func (r *Reporter) runLogProcessor() { for { select { - case line := <- r.tailer.Lines: + case line := <-r.tailer.Lines: if line == nil { continue } @@ -72,7 +72,7 @@ func (r *Reporter) runLogProcessor() { r.ReportingChannel <- report } } - case <- time.After(100 * time.Millisecond): + case <-time.After(100 * time.Millisecond): if !r.running { return } diff --git a/server.go b/server.go index b1d2c5b..e2daf91 100644 --- a/server.go +++ b/server.go @@ -2,41 +2,159 @@ package iperf import ( "context" + "encoding/json" "fmt" "github.com/google/uuid" "io" + "strings" "time" ) +var ( + defaultPort = 5201 + defaultInterval = 1 + defaultJSON = true +) + func NewServer() *Server { - defaultPort := 5201 - defaultInterval := 1 s := &Server{ - Port: &defaultPort, - Interval: &defaultInterval, + Options: &ServerOptions{ + Port: &defaultPort, + Interval: &defaultInterval, + JSON: &defaultJSON, + }, } s.Id = uuid.New().String() return s } +type ServerOptions struct { + OneOff *bool `json:"one_off" yaml:"one_off" xml:"one_off"` + Port *int `json:"port" yaml:"port" xml:"port"` + Format *rune `json:"format" yaml:"format" xml:"format"` + Interval *int `json:"interval" yaml:"interval" xml:"interval"` + JSON *bool `json:"json" yaml:"json" xml:"json"` + LogFile *string `json:"log_file" yaml:"log_file" xml:"log_file"` +} + type Server struct { Id string `json:"id" yaml:"id" xml:"id"` - OneOff *bool `json:"one_off" yaml:"one_off" xml:"one_off"` - ExitCode *int `json:"exit_code" yaml:"exit_code" xml:"exit_code"` - Port *int `json:"port" yaml:"port" xml:"port"` - Format *rune `json:"format" yaml:"format" xml:"format"` - Interval *int `json:"interval" yaml:"interval" xml:"interval"` - JSON *bool `json:"json" yaml:"json" xml:"json"` - LogFile *string `json:"log_file" yaml:"log_file" xml:"log_file"` Running bool `json:"running" yaml:"running" xml:"running"` + Options *ServerOptions `json:"-" yaml:"-" xml:"-"` + ExitCode *int `json:"exit_code" yaml:"exit_code" xml:"exit_code"` outputStream io.ReadCloser `json:"output_stream" yaml:"output_stream" xml:"output_stream"` errorStream io.ReadCloser `json:"error_stream" yaml:"error_stream" xml:"error_stream"` cancel context.CancelFunc `json:"cancel" yaml:"cancel" xml:"cancel"` } +func (s *Server) LoadOptionsJSON(jsonStr string) (err error) { + return json.Unmarshal([]byte(jsonStr), s.Options) +} + +func (s *Server) LoadOptions(options *ServerOptions) { + s.Options = options +} + +func (s *Server) commandString() (cmd string, err error) { + builder := strings.Builder{} + fmt.Fprintf(&builder, "%s -s", binaryLocation) + + if s.Options.OneOff != nil && s.OneOff() == true { + builder.WriteString(" --one-off") + } + + if s.Options.Port != nil { + fmt.Fprintf(&builder, " --port %d", s.Port()) + } + + if s.Options.Format != nil { + fmt.Fprintf(&builder, " --format %c", s.Format()) + } + + if s.Options.Interval != nil { + fmt.Fprintf(&builder, " --interval %d", s.Interval()) + } + + if s.Options.JSON != nil && s.JSON() == true { + builder.WriteString(" --json") + } + + if s.Options.LogFile != nil && s.LogFile() != "" { + fmt.Fprintf(&builder, " --logfile %s --forceflush", s.LogFile()) + } + + return builder.String(), nil +} + +func (s *Server) OneOff() bool { + if s.Options.OneOff == nil { + return false + } + return *s.Options.OneOff +} + +func (s *Server) SetOneOff(oneOff bool) { + s.Options.OneOff = &oneOff +} + +func (s *Server) Port() int { + if s.Options.Port == nil { + return defaultPort + } + return *s.Options.Port +} + +func (s *Server) SetPort(port int) { + s.Options.Port = &port +} + +func (s *Server) Format() rune { + if s.Options.Format == nil { + return ' ' + } + return *s.Options.Format +} + +func (s *Server) SetFormat(format rune) { + s.Options.Format = &format +} + +func (s *Server) Interval() int { + if s.Options.Interval == nil { + return defaultInterval + } + return *s.Options.Interval +} + +func (s *Server) JSON() bool { + if s.Options.JSON == nil { + return false + } + return *s.Options.JSON +} + +func (s *Server) SetJSON(json bool) { + s.Options.JSON = &json +} + +func (s *Server) LogFile() string { + if s.Options.LogFile == nil { + return "" + } + return *s.Options.LogFile +} + +func (s *Server) SetLogFile(filename string) { + s.Options.LogFile = &filename +} + func (s *Server) Start() (err error) { + cmd, err := s.commandString() + if err != nil { + return err + } var exit chan int - s.outputStream, s.errorStream, exit, s.cancel, err = ExecuteAsyncWithCancel(fmt.Sprintf("%s -s -J", binaryLocation)) + s.outputStream, s.errorStream, exit, s.cancel, err = ExecuteAsyncWithCancel(cmd) if err != nil { return err }