From 4bb2113e1642de4ef9ccbaf547057ab8f5ca64b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Mon, 4 May 2020 00:27:28 +0100 Subject: [PATCH] add network.[Must]GetDataNetworkIP; add popular param types. (#12) --- network/address.go | 59 +++++++++++++++++++++++++++++++++++++++++ network/types.go | 5 ++++ ptypes/doc.go | 2 ++ ptypes/duration.go | 42 +++++++++++++++++++++++++++++ ptypes/rate.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++ ptypes/size.go | 38 ++++++++++++++++++++++++++ 6 files changed, 212 insertions(+) create mode 100644 network/address.go create mode 100644 ptypes/doc.go create mode 100644 ptypes/duration.go create mode 100644 ptypes/rate.go create mode 100644 ptypes/size.go diff --git a/network/address.go b/network/address.go new file mode 100644 index 0000000..4d8b3f6 --- /dev/null +++ b/network/address.go @@ -0,0 +1,59 @@ +package network + +import ( + "fmt" + "net" +) + +// GetDataNetworkIP examines the local network interfaces, and tries to find our +// assigned IP within the data network. +// +// This function returns the IP and a nil error if found. If running in +// a sidecar-less environment, the error ErrNoTrafficShaping is returned. +func (c *Client) GetDataNetworkIP() (net.IP, error) { + re := c.runenv + if !re.TestSidecar { + return nil, ErrNoTrafficShaping + } + + ifaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("unable to get local network interfaces: %s", err) + } + + for _, i := range ifaces { + addrs, err := i.Addrs() + if err != nil { + re.RecordMessage("error getting addrs for interface: %s", err) + continue + } + for _, a := range addrs { + switch v := a.(type) { + case *net.IPNet: + ip := v.IP.To4() + if ip == nil { + re.RecordMessage("ignoring non ip4 addr %s", v) + continue + } + if re.TestSubnet.Contains(ip) { + re.RecordMessage("detected data network IP: %s", v) + return v.IP, nil + } else { + re.RecordMessage("%s not in data subnet %s, ignoring", ip, re.TestSubnet.String()) + } + } + } + } + return nil, fmt.Errorf("unable to determine data network IP. no interface found with IP in %s", re.TestSubnet.String()) +} + +// MustGetDataNetworkIP calls GetDataNetworkIP, and panics if it +// errors. It is suitable to use with runner.Invoke/InvokeMap, as long as +// this method is called from the main goroutine of the test plan. +func (c *Client) MustGetDataNetworkIP() net.IP { + ip, err := c.GetDataNetworkIP() + if err != nil { + panic(err) + } + return ip +} diff --git a/network/types.go b/network/types.go index a73aaa7..e9d1f6b 100644 --- a/network/types.go +++ b/network/types.go @@ -1,12 +1,17 @@ package network import ( + "fmt" "net" "time" "github.com/testground/sdk-go/sync" ) +// ErrNoTrafficShaping is returned from functions in this package when traffic +// shaping is not available, such as when using the local:exec runner. +var ErrNoTrafficShaping = fmt.Errorf("no traffic shaping available with this runner") + type FilterAction int const ( diff --git a/ptypes/doc.go b/ptypes/doc.go new file mode 100644 index 0000000..4aa951a --- /dev/null +++ b/ptypes/doc.go @@ -0,0 +1,2 @@ +// Package ptypes contains types that are commonplace in test plan parameters. +package ptypes diff --git a/ptypes/duration.go b/ptypes/duration.go new file mode 100644 index 0000000..5942c7a --- /dev/null +++ b/ptypes/duration.go @@ -0,0 +1,42 @@ +package ptypes + +import ( + "encoding/json" + "errors" + "time" +) + +// Duration wraps a time.Duration and provides JSON marshal logic. +type Duration struct { + time.Duration +} + +var ( + _ json.Marshaler = Duration{} + _ json.Unmarshaler = &Duration{} +) + +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +func (d *Duration) UnmarshalJSON(b []byte) error { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + switch value := v.(type) { + case float64: + d.Duration = time.Duration(value) + return nil + case string: + var err error + d.Duration, err = time.ParseDuration(value) + if err != nil { + return err + } + return nil + default: + return errors.New("invalid duration") + } +} diff --git a/ptypes/rate.go b/ptypes/rate.go new file mode 100644 index 0000000..08a581e --- /dev/null +++ b/ptypes/rate.go @@ -0,0 +1,66 @@ +package ptypes + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + "time" + "unicode" +) + +// Rate is a param that's parsed as "quantity/interval", where `quantity` is a +// float and `interval` is a string parsable by time.ParseDuration, e.g. "1s". +// +// You can omit the numeric component of the interval to default to 1, e.g. +// "100/s" is the same as "100/1s". +// +// Examples of valid Rate strings include: "100/s", "0.5/m", "500/5m". +type Rate struct { + Quantity float64 + Interval time.Duration +} + +var ( + _ json.Marshaler = Duration{} + _ json.Unmarshaler = &Duration{} +) + +func (r Rate) MarshalJSON() ([]byte, error) { + return nil, nil +} +func (r *Rate) UnmarshalJSON(b []byte) error { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + + str, ok := v.(string) + if !ok { + return errors.New("invalid rate param, must be string") + } + + strs := strings.Split(str, "/") + if len(strs) != 2 { + return errors.New("invalid rate param. Must be in format 'quantity / interval'") + } + + q, err := strconv.ParseFloat(strs[0], 64) + if err != nil { + return fmt.Errorf("error parsing quantity portion of rate: %s", err) + } + intervalStr := strings.TrimSpace(strs[1]) + if !unicode.IsDigit(rune(intervalStr[0])) { + intervalStr = "1" + intervalStr + } + + i, err := time.ParseDuration(intervalStr) + if err != nil { + return fmt.Errorf("error parsing interval portion of rate: %s", err) + } + + r.Quantity = q + r.Interval = i + return nil +} diff --git a/ptypes/size.go b/ptypes/size.go new file mode 100644 index 0000000..e1dd9ef --- /dev/null +++ b/ptypes/size.go @@ -0,0 +1,38 @@ +package ptypes + +import ( + "encoding/json" + "errors" + + "github.com/dustin/go-humanize" +) + +// Size is a type that unmarshals human-readable binary sizes like "100 KB" +// into an uint64, where the unit is bytes. +type Size uint64 + +var ( + _ json.Marshaler = Duration{} + _ json.Unmarshaler = &Duration{} +) + +func (s Size) MarshalJSON() ([]byte, error) { + return nil, nil +} + +func (s *Size) UnmarshalJSON(b []byte) error { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + str, ok := v.(string) + if !ok { + return errors.New("invalid size param, must be string") + } + n, err := humanize.ParseBytes(str) + if err != nil { + return err + } + *s = Size(n) + return nil +}