go.ligato.io/vpp-agent/v3@v3.5.0/tests/e2e/e2etest/vppagent.go (about)

     1  //  Copyright (c) 2020 Cisco and/or its affiliates.
     2  //
     3  //  Licensed under the Apache License, Version 2.0 (the "License");
     4  //  you may not use this file except in compliance with the License.
     5  //  You may obtain a copy of the License at:
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  //  Unless required by applicable law or agreed to in writing, software
    10  //  distributed under the License is distributed on an "AS IS" BASIS,
    11  //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  //  See the License for the specific language governing permissions and
    13  //  limitations under the License.
    14  
    15  package e2etest
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"path/filepath"
    21  	"regexp"
    22  	"strings"
    23  	"testing"
    24  
    25  	docker "github.com/fsouza/go-dockerclient"
    26  	"github.com/go-errors/errors"
    27  	"github.com/onsi/gomega"
    28  	"github.com/vishvananda/netns"
    29  	"go.ligato.io/cn-infra/v2/health/statuscheck/model/status"
    30  	"go.ligato.io/cn-infra/v2/logging"
    31  	"google.golang.org/grpc"
    32  	"google.golang.org/protobuf/proto"
    33  
    34  	"go.ligato.io/vpp-agent/v3/client"
    35  	"go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types"
    36  	ctl "go.ligato.io/vpp-agent/v3/cmd/agentctl/client"
    37  	"go.ligato.io/vpp-agent/v3/pkg/models"
    38  	kvs "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api"
    39  	"go.ligato.io/vpp-agent/v3/plugins/linux/ifplugin/linuxcalls"
    40  	"go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler"
    41  )
    42  
    43  const (
    44  	agentImage       = "ligato/vpp-agent:latest"
    45  	agentLabelKey    = "e2e.test.vppagent"
    46  	agentNamePrefix  = "e2e-test-vppagent-"
    47  	agentInitTimeout = 15 // seconds
    48  	agentStopTimeout = 3  // seconds
    49  )
    50  
    51  var vppPingRegexp = regexp.MustCompile("Statistics: ([0-9]+) sent, ([0-9]+) received, ([0-9]+)% packet loss")
    52  
    53  // Agent represents running VPP-Agent test component
    54  type Agent struct {
    55  	ComponentRuntime
    56  	client ctl.APIClient
    57  	ctx    *TestCtx
    58  	name   string
    59  }
    60  
    61  // NewAgent creates and starts new VPP-Agent container
    62  func NewAgent(ctx *TestCtx, name string, optMods ...AgentOptModifier) (*Agent, error) {
    63  	// compute options
    64  	opts := DefaultAgentOpt(ctx, name)
    65  	for _, mod := range optMods {
    66  		mod(opts)
    67  	}
    68  
    69  	// create struct for Agent
    70  	agent := &Agent{
    71  		ComponentRuntime: opts.Runtime,
    72  		ctx:              ctx,
    73  		name:             name,
    74  	}
    75  
    76  	// get runtime specific options and start agent in runtime environment
    77  	startOpts, err := opts.RuntimeStartOptions(ctx, opts)
    78  	if err != nil {
    79  		return nil, errors.Errorf("can't get agent %s start option for runtime due to: %v", name, err)
    80  	}
    81  	err = agent.Start(startOpts)
    82  	if err != nil {
    83  		return nil, errors.Errorf("can't start agent %s due to: %v", name, err)
    84  	}
    85  	agent.client, err = ctl.NewClient(agent.IPAddress())
    86  	if err != nil {
    87  		return nil, errors.Errorf("can't create client for %s due to: %v", name, err)
    88  	}
    89  
    90  	agent.ctx.Eventually(agent.checkReady, agentInitTimeout, checkPollingInterval).Should(gomega.Succeed())
    91  	if opts.InitialResync {
    92  		agent.Sync()
    93  	}
    94  	return agent, nil
    95  }
    96  
    97  func (agent *Agent) Stop(options ...interface{}) error {
    98  	if err := agent.ComponentRuntime.Stop(options); err != nil {
    99  		// not additionally cleaning up after attempting to stop test topology component because
   100  		// it would lock access to further inspection of this component (i.e. why it won't stop)
   101  		return err
   102  	}
   103  	// cleanup
   104  	if err := agent.client.Close(); err != nil {
   105  		return err
   106  	}
   107  	delete(agent.ctx.agents, agent.name)
   108  	return nil
   109  }
   110  
   111  // AgentStartOptionsForContainerRuntime translates AgentOpt to options for ComponentRuntime.Start(option)
   112  // method implemented by ContainerRuntime
   113  func AgentStartOptionsForContainerRuntime(ctx *TestCtx, options interface{}) (interface{}, error) {
   114  	opts, ok := options.(*AgentOpt)
   115  	if !ok {
   116  		return nil, errors.Errorf("expected AgentOpt but got %+v", options)
   117  	}
   118  
   119  	// construct vpp-agent container creation options
   120  	agentLabel := agentNamePrefix + opts.Name
   121  	createOpts := &docker.CreateContainerOptions{
   122  		Context: ctx.ctx,
   123  		Name:    agentLabel,
   124  		Config: &docker.Config{
   125  			Image: opts.Image,
   126  			Labels: map[string]string{
   127  				agentLabelKey: opts.Name,
   128  			},
   129  			Env:          opts.Env,
   130  			AttachStderr: true,
   131  			AttachStdout: true,
   132  		},
   133  		HostConfig: &docker.HostConfig{
   134  			PublishAllPorts: true,
   135  			Privileged:      true,
   136  			PidMode:         "host",
   137  			Binds: []string{
   138  				"/var/run/docker.sock:/var/run/docker.sock",
   139  				ctx.DataDir + ":/testdata:ro",
   140  				filepath.Join(ctx.DataDir, "certs") + ":/etc/certs:ro",
   141  				shareVolumeName + ":" + ctx.ShareDir,
   142  			},
   143  		},
   144  	}
   145  	if opts.ContainerOptsHook != nil {
   146  		opts.ContainerOptsHook(createOpts)
   147  	}
   148  	return &ContainerStartOptions{
   149  		ContainerOptions: createOpts,
   150  		Pull:             false,
   151  		AttachLogs:       true,
   152  	}, nil
   153  }
   154  
   155  // TODO this is runtime specific -> integrate it into runtime concept
   156  func removeDanglingAgents(t *testing.T, dockerClient *docker.Client) {
   157  	// remove any running vpp-agents prior to starting a new test
   158  	containers, err := dockerClient.ListContainers(docker.ListContainersOptions{
   159  		All: true,
   160  		Filters: map[string][]string{
   161  			"label": {agentLabelKey},
   162  		},
   163  	})
   164  	if err != nil {
   165  		t.Fatalf("failed to list existing vpp-agents: %v", err)
   166  	}
   167  	for _, container := range containers {
   168  		err = dockerClient.RemoveContainer(docker.RemoveContainerOptions{
   169  			ID:    container.ID,
   170  			Force: true,
   171  		})
   172  		if err != nil {
   173  			t.Fatalf("failed to remove existing vpp-agents: %v", err)
   174  		} else {
   175  			t.Logf("removed existing vpp-agent: %s", container.Labels[agentLabelKey])
   176  		}
   177  	}
   178  }
   179  
   180  func (agent *Agent) LinuxInterfaceHandler() linuxcalls.NetlinkAPI {
   181  	agent.ctx.t.Helper()
   182  	ns, err := netns.GetFromPid(agent.PID())
   183  	if err != nil {
   184  		agent.ctx.t.Fatalf("unable to get netns (PID %v)", agent.PID())
   185  	}
   186  	ifHandler := linuxcalls.NewNetLinkHandlerNs(ns, logging.DefaultLogger)
   187  	return ifHandler
   188  }
   189  
   190  func (agent *Agent) Client() ctl.APIClient {
   191  	return agent.client
   192  }
   193  
   194  // GenericClient provides generic client for communication with default VPP-Agent test component
   195  func (agent *Agent) GenericClient() client.GenericClient {
   196  	agent.ctx.t.Helper()
   197  	c, err := agent.client.GenericClient()
   198  	if err != nil {
   199  		agent.ctx.t.Fatalf("Failed to get generic VPP-agent client: %v", err)
   200  	}
   201  	return c
   202  }
   203  
   204  // GRPCConn provides GRPC client connection for communication with default VPP-Agent test component
   205  func (agent *Agent) GRPCConn() *grpc.ClientConn {
   206  	agent.ctx.t.Helper()
   207  	conn, err := agent.client.GRPCConn()
   208  	if err != nil {
   209  		agent.ctx.t.Fatalf("Failed to get gRPC connection: %v", err)
   210  	}
   211  	return conn
   212  }
   213  
   214  // Sync runs downstream resync and returns the list of executed operations.
   215  func (agent *Agent) Sync() kvs.RecordedTxnOps {
   216  	agent.ctx.t.Helper()
   217  	txn, err := agent.client.SchedulerResync(context.Background(), types.SchedulerResyncOptions{
   218  		Retry: true,
   219  	})
   220  	if err != nil {
   221  		agent.ctx.t.Fatalf("Downstream resync request has failed: %v", err)
   222  	}
   223  	if txn.Start.IsZero() {
   224  		agent.ctx.t.Fatalf("Downstream resync returned empty transaction record: %v", txn)
   225  	}
   226  	return txn.Executed
   227  }
   228  
   229  // IsInSync checks if the agent NB config and the SB state (VPP+Linux) are in-sync.
   230  func (agent *Agent) IsInSync() bool {
   231  	agent.ctx.t.Helper()
   232  	ops := agent.Sync()
   233  	for _, op := range ops {
   234  		if !op.NOOP {
   235  			return false
   236  		}
   237  	}
   238  	return true
   239  }
   240  
   241  func (agent *Agent) checkReady() error {
   242  	agentStatus, err := agent.client.Status(agent.ctx.ctx)
   243  	if err != nil {
   244  		return fmt.Errorf("query to get %s status failed: %v", agent.name, err)
   245  	}
   246  	agentPlugin, ok := agentStatus.PluginStatus["VPPAgent"]
   247  	if !ok {
   248  		return fmt.Errorf("%s plugin status missing", agent.name)
   249  	}
   250  	if agentPlugin.State != status.OperationalState_OK {
   251  		return fmt.Errorf("%s status: %v", agent.name, agentPlugin.State.String())
   252  	}
   253  	return nil
   254  }
   255  
   256  // ExecVppctl returns output from vppctl for given action and arguments.
   257  func (agent *Agent) ExecVppctl(action string, args ...string) (string, error) {
   258  	cmd := append([]string{"-s", "/run/vpp/cli.sock", action}, args...)
   259  	stdout, _, err := agent.ExecCmd("vppctl", cmd...)
   260  	if err != nil {
   261  		return "", fmt.Errorf("execute `vppctl %s` error: %v", strings.Join(cmd, " "), err)
   262  	}
   263  	if Debug {
   264  		agent.ctx.t.Logf("executed (vppctl %v): %v", strings.Join(cmd, " "), stdout)
   265  	}
   266  
   267  	return stdout, nil
   268  }
   269  
   270  // PingFromVPPAsCallback can be used to ping repeatedly inside the assertions "Eventually"
   271  // and "Consistently" from Omega.
   272  func (agent *Agent) PingFromVPPAsCallback(destAddress string, args ...string) func() error {
   273  	return func() error {
   274  		return agent.PingFromVPP(destAddress, args...)
   275  	}
   276  }
   277  
   278  // PingFromVPP pings <dstAddress> from inside the VPP.
   279  func (agent *Agent) PingFromVPP(destAddress string, args ...string) error {
   280  	// run ping on VPP using vppctl
   281  	stdout, err := agent.ExecVppctl("ping", append([]string{destAddress}, args...)...)
   282  	if err != nil {
   283  		return err
   284  	}
   285  
   286  	// parse output
   287  	matches := vppPingRegexp.FindStringSubmatch(stdout)
   288  	sent, recv, loss, err := parsePingOutput(stdout, matches)
   289  	if err != nil {
   290  		return err
   291  	}
   292  	agent.ctx.Logger.Printf("VPP ping %s: sent=%d, received=%d, loss=%d%%",
   293  		destAddress, sent, recv, loss)
   294  
   295  	if sent == 0 || loss >= 50 {
   296  		return fmt.Errorf("failed to ping '%s': %s", destAddress, matches[0])
   297  	}
   298  	return nil
   299  }
   300  
   301  func (agent *Agent) getKVDump(value proto.Message, view kvs.View) []kvs.RecordedKVWithMetadata {
   302  	agent.ctx.t.Helper()
   303  	model, err := models.GetModelFor(value)
   304  	if err != nil {
   305  		agent.ctx.t.Fatalf("Failed to get model for value %v: %v", value, err)
   306  	}
   307  	kvDump, err := agent.client.SchedulerDump(context.Background(), types.SchedulerDumpOptions{
   308  		KeyPrefix: model.KeyPrefix(),
   309  		View:      view.String(),
   310  	})
   311  	if err != nil {
   312  		agent.ctx.t.Fatalf("Request to dump values failed: %v", err)
   313  	}
   314  	return kvDump
   315  }
   316  
   317  // GetValue retrieves value(s) as seen by the given view
   318  func (agent *Agent) GetValue(value proto.Message, view kvs.View) proto.Message {
   319  	agent.ctx.t.Helper()
   320  	key, err := models.GetKey(value)
   321  	if err != nil {
   322  		agent.ctx.t.Fatalf("Failed to get key for value %v: %v", value, err)
   323  	}
   324  	kvDump := agent.getKVDump(value, view)
   325  	for _, kv := range kvDump {
   326  		if kv.Key == key {
   327  			return kv.Value.Message
   328  		}
   329  	}
   330  	return nil
   331  }
   332  
   333  // GetValueMetadata retrieves metadata associated with the given value.
   334  func (agent *Agent) GetValueMetadata(value proto.Message, view kvs.View) (metadata interface{}) {
   335  	agent.ctx.t.Helper()
   336  	key, err := models.GetKey(value)
   337  	if err != nil {
   338  		agent.ctx.t.Fatalf("Failed to get key for value %v: %v", value, err)
   339  	}
   340  	kvDump := agent.getKVDump(value, view)
   341  	for _, kv := range kvDump {
   342  		if kv.Key == key {
   343  			return kv.Metadata
   344  		}
   345  	}
   346  	return nil
   347  }
   348  
   349  // NumValues returns number of values found under the given model
   350  func (agent *Agent) NumValues(value proto.Message, view kvs.View) int {
   351  	agent.ctx.t.Helper()
   352  	return len(agent.getKVDump(value, view))
   353  }
   354  
   355  func (agent *Agent) getValueStateByKey(key, derivedKey string) kvscheduler.ValueState {
   356  	agent.ctx.t.Helper()
   357  	values, err := agent.client.SchedulerValues(context.Background(), types.SchedulerValuesOptions{
   358  		Key: key,
   359  	})
   360  	if err != nil {
   361  		agent.ctx.t.Fatalf("Request to obtain value status has failed: %v", err)
   362  	}
   363  	if len(values) != 1 {
   364  		agent.ctx.t.Fatalf("Expected single value status, got status for %d values", len(values))
   365  	}
   366  	st := values[0]
   367  	if st.GetValue().GetKey() != key {
   368  		agent.ctx.t.Fatalf("Received value status for unexpected key: %v", st)
   369  	}
   370  	if derivedKey != "" {
   371  		for _, derVal := range st.DerivedValues {
   372  			if derVal.Key == derivedKey {
   373  				return derVal.State
   374  			}
   375  		}
   376  		return kvscheduler.ValueState_NONEXISTENT
   377  	}
   378  	return st.GetValue().GetState()
   379  }
   380  
   381  func (agent *Agent) GetValueState(value proto.Message) kvscheduler.ValueState {
   382  	agent.ctx.t.Helper()
   383  	key := models.Key(value)
   384  	return agent.getValueStateByKey(key, "")
   385  }
   386  
   387  func (agent *Agent) GetValueStateClb(value proto.Message) func() kvscheduler.ValueState {
   388  	return func() kvscheduler.ValueState {
   389  		return agent.GetValueState(value)
   390  	}
   391  }
   392  
   393  func (agent *Agent) GetValueStateByKey(key string) kvscheduler.ValueState {
   394  	agent.ctx.t.Helper()
   395  	return agent.getValueStateByKey(key, "")
   396  }
   397  
   398  func (agent *Agent) GetValueStateByKeyClb(key string) func() kvscheduler.ValueState {
   399  	return func() kvscheduler.ValueState {
   400  		return agent.GetValueStateByKey(key)
   401  	}
   402  }
   403  
   404  func (agent *Agent) GetDerivedValueState(baseValue proto.Message, derivedKey string) kvscheduler.ValueState {
   405  	agent.ctx.t.Helper()
   406  	key := models.Key(baseValue)
   407  	return agent.getValueStateByKey(key, derivedKey)
   408  }
   409  
   410  func (agent *Agent) GetDerivedValueStateClb(baseValue proto.Message, derivedKey string) func() kvscheduler.ValueState {
   411  	return func() kvscheduler.ValueState {
   412  		return agent.GetDerivedValueState(baseValue, derivedKey)
   413  	}
   414  }