mirror of
https://github.com/hanzozt/zt.git
synced 2026-11-03 05:01:58 +00:00
398 lines
11 KiB
Go
398 lines
11 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package loop4
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"sort"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/agent"
|
|
"github.com/openziti/foundation/v2/util"
|
|
"github.com/openziti/identity"
|
|
"github.com/openziti/identity/dotziti"
|
|
"github.com/openziti/metrics"
|
|
"github.com/openziti/sdk-golang/ziti"
|
|
"github.com/openziti/transport/v2"
|
|
"github.com/openziti/ziti/common/outputz"
|
|
trafficMetrics "github.com/openziti/ziti/zititest/ziti-traffic-test/metrics"
|
|
"github.com/openziti/ziti/zititest/ziti-traffic-test/subcmd"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func init() {
|
|
subcmd.Root.AddCommand(loop4Cmd)
|
|
}
|
|
|
|
var loop4Cmd = &cobra.Command{
|
|
Use: "loop4",
|
|
Short: "Loop testing tool, v4",
|
|
}
|
|
|
|
func NewSim() *Sim {
|
|
return &Sim{
|
|
sdkClients: make(map[string]ziti.Context),
|
|
dialers: make(map[string]func(workload *Workload) (net.Conn, error)),
|
|
listeners: make(map[string]func(workload *Workload) (net.Listener, error)),
|
|
closeNotify: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
type Sim struct {
|
|
sdkClients map[string]ziti.Context
|
|
dialers map[string]func(workload *Workload) (net.Conn, error)
|
|
listeners map[string]func(workload *Workload) (net.Listener, error)
|
|
sdkMetricsStartHooks []func(ctx ziti.Context) error
|
|
closeNotify chan struct{}
|
|
scenario *Scenario
|
|
metrics metrics.Registry
|
|
}
|
|
|
|
func (sim *Sim) InitScenario(path string) error {
|
|
log := pfxlog.Logger()
|
|
|
|
shutdownClean := false
|
|
if err := agent.Listen(agent.Options{ShutdownCleanup: &shutdownClean}); err != nil {
|
|
log.WithError(err).Error("unable to start CLI agent")
|
|
}
|
|
|
|
var err error
|
|
|
|
sim.scenario, err = LoadScenario(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debug(sim.scenario)
|
|
|
|
if err = sim.InitConnectors(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = sim.StartMetrics(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sim *Sim) InitConnectors() error {
|
|
log := pfxlog.Logger()
|
|
log.Infof("loading %d connectors", len(sim.scenario.ConnectorConfigs))
|
|
for name, connector := range sim.scenario.ConnectorConfigs {
|
|
if connector.SdkOptions != nil {
|
|
log.Infof("loading sdk connector '%s'", name)
|
|
cfg, err := ziti.NewConfigFromFile(connector.SdkOptions.IdentityFile)
|
|
if err != nil {
|
|
return fmt.Errorf("for connector '%s', unable to load ziti identity config file '%s' (%w)",
|
|
name, connector.SdkOptions.IdentityFile, err)
|
|
}
|
|
|
|
cfg.SetMaxControlConnections(connector.SdkOptions.MaxControlUnderlays)
|
|
cfg.SetMaxDefaultConnections(connector.SdkOptions.MaxDataUnderlays)
|
|
|
|
ctx, err := ziti.NewContext(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("for connector '%s', unable to create ziti sdk context for identity file '%s' (%w)",
|
|
name, connector.SdkOptions.IdentityFile, err)
|
|
}
|
|
|
|
if connector.SdkOptions.TestService != "" {
|
|
conn, err := ctx.Dial(connector.SdkOptions.TestService)
|
|
if conn != nil {
|
|
_ = conn.Close()
|
|
} else {
|
|
log.WithError(err).Info("unable to dial test service")
|
|
}
|
|
}
|
|
|
|
sim.sdkClients[name] = ctx
|
|
|
|
if connector.SdkOptions.ReportSdkMetrics {
|
|
sim.sdkMetricsStartHooks = append(sim.sdkMetricsStartHooks, func(metricsCtx ziti.Context) error {
|
|
return sim.StartSdkMetricsReporter(ctx.Metrics(), metricsCtx)
|
|
})
|
|
}
|
|
|
|
sim.dialers[name] = func(workload *Workload) (net.Conn, error) {
|
|
dialOptions := &ziti.DialOptions{
|
|
ConnectTimeout: workload.ConnectTimeout,
|
|
SdkFlowControl: &connector.SdkOptions.EnableSdkXgress,
|
|
}
|
|
return ctx.DialWithOptions(workload.ServiceName, dialOptions)
|
|
}
|
|
|
|
sim.listeners[name] = func(workload *Workload) (net.Listener, error) {
|
|
return sim.ListenSdk(ctx, workload, connector.SdkOptions.EnableSdkXgress)
|
|
}
|
|
} else if connector.TransportOptions != nil {
|
|
log.Infof("loading transport connector '%s'", name)
|
|
addr, err := transport.ParseAddress(connector.TransportOptions.Address)
|
|
if err != nil {
|
|
return fmt.Errorf("for connector '%s', unable to parse transport address '%s' (%w)",
|
|
name, connector.TransportOptions.Address, err)
|
|
}
|
|
|
|
id := &identity.TokenId{Token: "test"}
|
|
if addr.Type() != "tcp" {
|
|
if _, id, err = dotziti.LoadIdentity(connector.TransportOptions.IdentityFile); err != nil {
|
|
return fmt.Errorf("for connector '%s', unable to load identity file '%s' (%w)",
|
|
name, connector.TransportOptions.IdentityFile, err)
|
|
}
|
|
}
|
|
|
|
sim.dialers[name] = func(workload *Workload) (net.Conn, error) {
|
|
return sim.DialTransport(addr, id, workload)
|
|
}
|
|
|
|
sim.listeners[name] = func(workload *Workload) (net.Listener, error) {
|
|
log.Infof("starting transport listener '%s' on '%s'", name, addr)
|
|
return sim.ListenTransport(addr, id, workload)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("connector '%s' has no configuration for sdk or transport", name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sim *Sim) ListenSdk(client ziti.Context, wf *Workload, useSdkFlowControl bool) (net.Listener, error) {
|
|
listenOptions := ziti.DefaultListenOptions()
|
|
listenOptions.SdkFlowControl = util.Ptr(useSdkFlowControl)
|
|
return client.ListenWithOptions(wf.ServiceName, listenOptions)
|
|
}
|
|
|
|
func (sim *Sim) DialTransport(addr transport.Address, id *identity.TokenId, wf *Workload) (net.Conn, error) {
|
|
return addr.Dial("name", id, wf.ConnectTimeout, nil)
|
|
}
|
|
|
|
func (sim *Sim) ListenTransport(addr transport.Address, id *identity.TokenId, wf *Workload) (net.Listener, error) {
|
|
adapter := &ListenerAdapter{
|
|
addr: addr,
|
|
connC: make(chan net.Conn, 1),
|
|
closeNotify: make(chan struct{}),
|
|
}
|
|
|
|
closer, err := addr.Listen(wf.Name, id, adapter.connReceived, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
adapter.closer = closer
|
|
|
|
return adapter, nil
|
|
}
|
|
|
|
func (sim *Sim) StartMetrics() error {
|
|
if sim.scenario.Metrics != nil {
|
|
if sim.scenario.Metrics.Connector == "" {
|
|
return errors.New("no sdk connector defined for metrics")
|
|
}
|
|
|
|
sim.metrics = metrics.NewRegistry(sim.scenario.Metrics.ClientId, nil)
|
|
|
|
client, ok := sim.sdkClients[sim.scenario.Metrics.Connector]
|
|
if !ok {
|
|
return fmt.Errorf("sdk connector selected for metrics '%s' does not defined", sim.scenario.Metrics.Connector)
|
|
}
|
|
|
|
if err := sim.StartSdkMetricsReporter(sim.metrics, client); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, initHook := range sim.sdkMetricsStartHooks {
|
|
if err := initHook(client); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if sim.scenario.Metrics != nil && sim.scenario.Metrics.WriteToStdout {
|
|
if sim.metrics == nil {
|
|
sim.metrics = metrics.NewRegistry("no-op", nil)
|
|
}
|
|
|
|
sim.StartStdoutMetricsReporter(sim.metrics)
|
|
for _, ctx := range sim.sdkClients {
|
|
sim.StartStdoutMetricsReporter(ctx.Metrics())
|
|
}
|
|
}
|
|
|
|
if sim.metrics == nil {
|
|
sim.metrics = metrics.NewRegistry("no-op", nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sim *Sim) StartSdkMetricsReporter(registry metrics.Registry, ctx ziti.Context) error {
|
|
if sim.scenario.Metrics.ReportInterval == 0 {
|
|
return errors.New("metrics report interval must be greater than 0")
|
|
}
|
|
|
|
cfg := &trafficMetrics.ZitiReporterConfig{
|
|
Registry: registry,
|
|
Client: ctx,
|
|
ClientId: sim.scenario.Metrics.ClientId,
|
|
CloseNotify: sim.closeNotify,
|
|
ServiceName: sim.scenario.Metrics.Service,
|
|
}
|
|
|
|
reporter, err := trafficMetrics.NewZitiReporter(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
go reporter.Run(sim.scenario.Metrics.ReportInterval)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sim *Sim) StartStdoutMetricsReporter(registry metrics.Registry) {
|
|
reporter := metrics.NewDelegatingReporter(registry, &metricsEmitter{}, sim.closeNotify)
|
|
go reporter.Start(15 * time.Second)
|
|
}
|
|
|
|
type metricsEmitter struct {
|
|
msgs []string
|
|
}
|
|
|
|
func (em *metricsEmitter) Filter(name string) bool {
|
|
return strings.Contains(name, metrics.MetricNameCount) ||
|
|
strings.Contains(name, metrics.MetricNameRateM1) ||
|
|
strings.Contains(name, metrics.MetricNameMean) ||
|
|
strings.Contains(name, metrics.MetricNamePercentile)
|
|
}
|
|
|
|
func (em *metricsEmitter) StartReport(metrics.Registry) {}
|
|
|
|
func (em *metricsEmitter) EndReport(metrics.Registry) {
|
|
sort.Strings(em.msgs)
|
|
for _, msg := range em.msgs {
|
|
fmt.Println(msg)
|
|
}
|
|
em.msgs = nil
|
|
}
|
|
|
|
func (em *metricsEmitter) AcceptIntMetric(name string, value int64) {
|
|
em.Printf("%s: %v", name, value)
|
|
}
|
|
|
|
func (em *metricsEmitter) AcceptFloatMetric(name string, value float64) {
|
|
if strings.HasSuffix(name, metrics.MetricNameRateM1) && strings.Contains(name, "bytes") {
|
|
em.Printf("%s: %v", name, outputz.FormatBytes(uint64(value)))
|
|
}
|
|
em.Printf("%s: %v", name, value)
|
|
}
|
|
|
|
func (em *metricsEmitter) AcceptPercentileMetric(name string, value metrics.PercentileSource) {
|
|
em.Printf("%s.p95: %v", name, time.Duration(value.Percentile(0.95)))
|
|
}
|
|
|
|
func (em *metricsEmitter) Printf(msg string, args ...interface{}) {
|
|
em.msgs = append(em.msgs, fmt.Sprintf(msg, args...))
|
|
}
|
|
|
|
type ListenerAdapter struct {
|
|
addr transport.Address
|
|
connC chan net.Conn
|
|
closer io.Closer
|
|
closeNotify chan struct{}
|
|
closed atomic.Bool
|
|
}
|
|
|
|
func (self *ListenerAdapter) Network() string {
|
|
return "tcp"
|
|
}
|
|
|
|
func (self *ListenerAdapter) String() string {
|
|
return self.addr.String()
|
|
}
|
|
|
|
func (self *ListenerAdapter) connReceived(conn transport.Conn) {
|
|
select {
|
|
case self.connC <- conn:
|
|
case <-self.closeNotify:
|
|
return
|
|
}
|
|
}
|
|
|
|
func (self *ListenerAdapter) Accept() (net.Conn, error) {
|
|
select {
|
|
case conn := <-self.connC:
|
|
return conn, nil
|
|
case <-self.closeNotify:
|
|
return nil, io.EOF
|
|
}
|
|
}
|
|
|
|
func (self *ListenerAdapter) Close() error {
|
|
if self.closed.CompareAndSwap(false, true) {
|
|
close(self.closeNotify)
|
|
return self.closer.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *ListenerAdapter) Addr() net.Addr {
|
|
return self
|
|
}
|
|
|
|
type dialResult struct {
|
|
conn net.Conn
|
|
err error
|
|
}
|
|
|
|
type AdvanceDialer struct {
|
|
dialer func() (net.Conn, error)
|
|
connCh chan dialResult
|
|
closeNotify chan struct{}
|
|
started atomic.Bool
|
|
}
|
|
|
|
func (self *AdvanceDialer) Dial(workload *Workload) (net.Conn, error) {
|
|
if self.started.CompareAndSwap(false, true) {
|
|
go self.Run()
|
|
}
|
|
|
|
select {
|
|
case result := <-self.connCh:
|
|
return result.conn, result.err
|
|
case <-time.After(workload.ConnectTimeout):
|
|
return nil, errors.New("dial timed out")
|
|
case <-self.closeNotify:
|
|
return nil, errors.New("sim stopped")
|
|
}
|
|
}
|
|
|
|
func (self *AdvanceDialer) Run() {
|
|
for {
|
|
conn, err := self.dialer()
|
|
select {
|
|
case self.connCh <- dialResult{conn, err}:
|
|
case <-self.closeNotify:
|
|
return
|
|
}
|
|
}
|
|
}
|