go.ligato.io/vpp-agent/v3@v3.5.0/tests/integration/vpp/integration_test.go (about)

     1  //  Copyright (c) 2018 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 vpp
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"os/exec"
    25  	"path"
    26  	"reflect"
    27  	"strings"
    28  	"syscall"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/mitchellh/go-ps"
    33  	. "github.com/onsi/gomega"
    34  	"go.fd.io/govpp/adapter"
    35  	"go.fd.io/govpp/adapter/socketclient"
    36  	"go.fd.io/govpp/adapter/statsclient"
    37  	govppapi "go.fd.io/govpp/api"
    38  	govppcore "go.fd.io/govpp/core"
    39  
    40  	"go.ligato.io/vpp-agent/v3/plugins/govppmux/vppcalls"
    41  	"go.ligato.io/vpp-agent/v3/plugins/vpp"
    42  	"go.ligato.io/vpp-agent/v3/plugins/vpp/binapi"
    43  )
    44  
    45  const (
    46  	vppConnectRetryDelay = time.Millisecond * 500
    47  	vppBootDelay         = time.Millisecond * 200
    48  	vppTermDelay         = time.Millisecond * 50
    49  	vppExitTimeout       = time.Second * 1
    50  
    51  	defaultVPPConfig = `
    52  		unix {
    53  			nodaemon
    54  			cli-listen /run/vpp/cli.sock
    55  			cli-no-pager
    56  			log /tmp/vpp.log
    57  			full-coredump
    58  		}
    59  		api-trace {
    60  			on
    61  		}
    62  		socksvr {
    63  			socket-name /run/vpp/api.sock
    64  		}
    65  		statseg {
    66  			socket-name /run/vpp/stats.sock
    67  			per-node-counters on
    68  		}
    69  		plugins {
    70  			plugin dpdk_plugin.so { disable }
    71  		}`
    72  	// in older versions of VPP (<=20.09), NAT plugin was also configured via the startup config file
    73  	withNatStartupConf = `
    74  		nat {
    75  			endpoint-dependent
    76  		}`
    77  )
    78  
    79  type TestCtx struct {
    80  	t              *testing.T
    81  	Ctx            context.Context
    82  	vppCmd         *exec.Cmd
    83  	stderr, stdout *bytes.Buffer
    84  	Conn           *govppcore.Connection
    85  	StatsConn      *govppcore.StatsConnection
    86  	vppBinapi      govppapi.Channel
    87  	vppStats       govppapi.StatsProvider
    88  	vpp            vppcalls.VppCoreAPI
    89  	versionInfo    *vppcalls.VersionInfo
    90  	vppClient      *vppClient
    91  }
    92  
    93  func startVPP(t *testing.T, stdout, stderr io.Writer) *exec.Cmd {
    94  	// check if VPP process is not running already
    95  	processes, err := ps.Processes()
    96  	if err != nil {
    97  		t.Fatalf("listing processes failed: %v", err)
    98  	}
    99  	for _, process := range processes {
   100  		proc := process.Executable()
   101  		if strings.Contains(proc, "vpp") && process.Pid() != os.Getpid() {
   102  			t.Logf(" - found process: %+v", process)
   103  		}
   104  		switch proc {
   105  		case *vppPath, "vpp", "vpp_main":
   106  			t.Fatalf("VPP is already running (PID: %v)", process.Pid())
   107  		}
   108  	}
   109  
   110  	// remove binapi files from previous run
   111  	var removeFile = func(path string) {
   112  		if err := os.Remove(path); err == nil {
   113  			t.Logf("removed file %q", path)
   114  		} else if !os.IsNotExist(err) {
   115  			t.Fatalf("removing file %q failed: %v", path, err)
   116  		}
   117  	}
   118  	removeFile(*vppSockAddr)
   119  
   120  	// ensure VPP runtime directory exists
   121  	if err := os.Mkdir("/run/vpp", 0755); err != nil && !os.IsExist(err) {
   122  		t.Logf("mkdir failed: %v", err)
   123  	}
   124  
   125  	// setup VPP process
   126  	vppCmd := exec.Command(*vppPath)
   127  	if *vppConfig != "" {
   128  		vppCmd.Args = append(vppCmd.Args, "-c", *vppConfig)
   129  	} else {
   130  		config := defaultVPPConfig
   131  		if os.Getenv("VPPVER") <= "20.09" {
   132  			config += withNatStartupConf
   133  		}
   134  		vppCmd.Args = append(vppCmd.Args, config)
   135  	}
   136  	if *debug {
   137  		vppCmd.Stderr = os.Stderr
   138  		vppCmd.Stdout = os.Stdout
   139  	} else {
   140  		vppCmd.Stderr = stderr
   141  		vppCmd.Stdout = stdout
   142  	}
   143  
   144  	// ensure that process is killed when current process exits
   145  	vppCmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
   146  
   147  	if err := vppCmd.Start(); err != nil {
   148  		t.Fatalf("starting VPP failed: %v", err)
   149  	}
   150  
   151  	t.Logf("VPP start OK (PID: %v)", vppCmd.Process.Pid)
   152  	return vppCmd
   153  }
   154  
   155  // reRegisterMessage overwrites the original registration of Messages in GoVPP with new message registration.
   156  func reRegisterMessage(x govppapi.Message) {
   157  	typ := reflect.TypeOf(x)
   158  	namecrc := x.GetMessageName() + "_" + x.GetCrcString()
   159  	binapiPath := path.Dir(reflect.TypeOf(x).Elem().PkgPath())
   160  	govppapi.GetRegisteredMessages()[binapiPath][namecrc] = x
   161  	govppapi.GetRegisteredMessageTypes()[binapiPath][typ] = namecrc
   162  }
   163  
   164  func hackForBugInGoVPPMessageCache(t *testing.T, adapter adapter.VppAPI, vppCmd *exec.Cmd) error {
   165  	// connect to VPP
   166  	conn, apiChannel, _ := connectToBinAPI(t, adapter, vppCmd)
   167  	binapiVersion, err := binapi.CompatibleVersion(apiChannel)
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	// overwrite messages with messages from correct VPP version
   173  	for _, msg := range binapi.Versions[binapiVersion].AllMessages() {
   174  		reRegisterMessage(msg)
   175  	}
   176  
   177  	// disconnect from VPP (GoVPP is caching the messages that we want to override
   178  	// by first connection to VPP -> we must disconnect and reconnect later again)
   179  	disconnectBinAPI(t, conn, apiChannel, nil)
   180  
   181  	return nil
   182  }
   183  
   184  func setupVPP(t *testing.T) *TestCtx {
   185  	RegisterTestingT(t)
   186  
   187  	start := time.Now()
   188  
   189  	ctx := context.TODO()
   190  
   191  	// start VPP process
   192  	var stderr, stdout bytes.Buffer
   193  	vppCmd := startVPP(t, &stdout, &stderr)
   194  	vppPID := uint32(vppCmd.Process.Pid)
   195  
   196  	// if setupVPP fails we need stop the VPP process
   197  	defer func() {
   198  		if t.Failed() {
   199  			stopVPP(t, vppCmd)
   200  		}
   201  	}()
   202  
   203  	// wait until the socket is ready
   204  	adapter := socketclient.NewVppClient(*vppSockAddr)
   205  	if err := adapter.WaitReady(); err != nil {
   206  		t.Logf("WaitReady error: %v", err)
   207  	}
   208  	time.Sleep(vppBootDelay)
   209  
   210  	// FIXME: this is a hack for GoVPP bug when register of the same message(same CRC and name) but different
   211  	//  VPP version overwrites the already registered message from one VPP version (map key is only CRC+name
   212  	//  and that didn't change with VPP version, but generated binapi generated 2 different go types for it).
   213  	//  Similar fix exists also for govppmux.
   214  	if err := hackForBugInGoVPPMessageCache(t, adapter, vppCmd); err != nil {
   215  		t.Fatal("can't apply hack fixing bug in GoVPP regarding stream's message type resolving")
   216  	}
   217  
   218  	// connect to VPP's binary API
   219  	conn, apiChannel, vppClient := connectToBinAPI(t, adapter, vppCmd)
   220  	vpeHandler := vppcalls.CompatibleHandler(vppClient)
   221  
   222  	// retrieve VPP version
   223  	versionInfo, err := vpeHandler.GetVersion(ctx)
   224  	if err != nil {
   225  		t.Fatalf("getting version info failed: %v", err)
   226  	}
   227  	t.Logf("VPP version: %v", versionInfo.Version)
   228  	if versionInfo.Version == "" {
   229  		t.Fatal("expected VPP version to not be empty")
   230  	}
   231  	// verify connected session
   232  	vpeInfo, err := vpeHandler.GetSession(ctx)
   233  	if err != nil {
   234  		t.Fatalf("getting vpp info failed: %v", err)
   235  	}
   236  	if vpeInfo.PID != vppPID {
   237  		t.Fatalf("expected VPP PID to be %v, got %v", vppPID, vpeInfo.PID)
   238  	}
   239  
   240  	vppClient.vpp = vpeHandler
   241  
   242  	// connect to stats
   243  	statsClient := statsclient.NewStatsClient("")
   244  	statsConn, err := govppcore.ConnectStats(statsClient)
   245  	if err != nil {
   246  		t.Logf("connecting to VPP stats API failed: %v", err)
   247  	} else {
   248  		vppClient.stats = statsConn
   249  	}
   250  
   251  	t.Logf("-> VPP ready (took %v)", time.Since(start).Seconds())
   252  
   253  	return &TestCtx{
   254  		t:           t,
   255  		Ctx:         ctx,
   256  		versionInfo: versionInfo,
   257  		vpp:         vpeHandler,
   258  		vppCmd:      vppCmd,
   259  		stderr:      &stderr,
   260  		stdout:      &stdout,
   261  		Conn:        conn,
   262  		vppBinapi:   apiChannel,
   263  		vppStats:    statsConn,
   264  		vppClient:   vppClient,
   265  	}
   266  }
   267  
   268  func connectToBinAPI(t *testing.T, adapter adapter.VppAPI, vppCmd *exec.Cmd) (*govppcore.Connection, govppapi.Channel, *vppClient) {
   269  	connectRetry := func(retries int) (conn *govppcore.Connection, err error) {
   270  		for i := 1; i <= retries; i++ {
   271  			conn, err = govppcore.Connect(adapter)
   272  			if err != nil {
   273  				t.Logf("attempt #%d failed: %v, retrying in %v", i, err, vppConnectRetryDelay)
   274  				time.Sleep(vppConnectRetryDelay)
   275  				continue
   276  			}
   277  			return
   278  		}
   279  		return nil, fmt.Errorf("failed to connect after %d retries", retries)
   280  	}
   281  
   282  	// connect to binapi
   283  	conn, err := connectRetry(int(*vppRetry))
   284  	if err != nil {
   285  		t.Errorf("connecting to VPP failed: %v", err)
   286  		if err := vppCmd.Process.Kill(); err != nil {
   287  			t.Fatalf("killing VPP failed: %v", err)
   288  		}
   289  		if state, err := vppCmd.Process.Wait(); err != nil {
   290  			t.Logf("VPP wait failed: %v", err)
   291  		} else {
   292  			t.Logf("VPP wait OK: %v", state)
   293  		}
   294  		t.FailNow()
   295  	}
   296  
   297  	apiChannel, err := conn.NewAPIChannel()
   298  	if err != nil {
   299  		t.Fatalf("creating channel failed: %v", err)
   300  	}
   301  
   302  	vppClient := &vppClient{
   303  		t:    t,
   304  		conn: conn,
   305  		ch:   apiChannel,
   306  	}
   307  	return conn, apiChannel, vppClient
   308  }
   309  
   310  func (ctx *TestCtx) teardownVPP() {
   311  	disconnectBinAPI(ctx.t, ctx.Conn, ctx.vppBinapi, ctx.StatsConn)
   312  	stopVPP(ctx.t, ctx.vppCmd)
   313  }
   314  
   315  func disconnectBinAPI(t *testing.T, conn *govppcore.Connection, vppBinapi govppapi.Channel,
   316  	statsConn *govppcore.StatsConnection) {
   317  	// disconnect sometimes hangs
   318  	done := make(chan struct{})
   319  	go func() {
   320  		if statsConn != nil {
   321  			statsConn.Disconnect()
   322  		}
   323  		vppBinapi.Close()
   324  		conn.Disconnect()
   325  		close(done)
   326  	}()
   327  	select {
   328  	case <-done:
   329  		time.Sleep(vppTermDelay)
   330  	case <-time.After(vppExitTimeout):
   331  		t.Logf("VPP disconnect timeout")
   332  	}
   333  }
   334  
   335  func stopVPP(t *testing.T, vppCmd *exec.Cmd) {
   336  	if err := vppCmd.Process.Signal(syscall.SIGTERM); err != nil {
   337  		t.Fatalf("sending SIGTERM to VPP failed: %v", err)
   338  	}
   339  	// wait until VPP exits
   340  	exit := make(chan struct{})
   341  	go func() {
   342  		if err := vppCmd.Wait(); err != nil {
   343  			var exiterr *exec.ExitError
   344  			if errors.As(err, &exiterr) && strings.Contains(exiterr.Error(), "core dumped") {
   345  				t.Logf("VPP process CRASHED: %s", exiterr.Error())
   346  			} else {
   347  				t.Logf("VPP process wait failed: %v", err)
   348  			}
   349  		} else {
   350  			t.Logf("VPP exit OK")
   351  		}
   352  		close(exit)
   353  	}()
   354  	select {
   355  	case <-exit:
   356  		// exited
   357  	case <-time.After(vppExitTimeout):
   358  		t.Logf("VPP exit timeout")
   359  		t.Logf("sending SIGKILL to VPP..")
   360  		if err := vppCmd.Process.Signal(syscall.SIGKILL); err != nil {
   361  			t.Fatalf("sending SIGKILL to VPP failed: %v", err)
   362  		}
   363  	}
   364  }
   365  
   366  type vppClient struct {
   367  	t       *testing.T
   368  	conn    *govppcore.Connection
   369  	ch      govppapi.Channel
   370  	stats   govppapi.StatsProvider
   371  	vpp     vppcalls.VppCoreAPI
   372  	version vpp.Version
   373  }
   374  
   375  func (v *vppClient) NewAPIChannel() (govppapi.Channel, error) {
   376  	return v.conn.NewAPIChannel()
   377  }
   378  
   379  func (v *vppClient) NewStream(ctx context.Context, options ...govppapi.StreamOption) (govppapi.Stream, error) {
   380  	return v.conn.NewStream(ctx, options...)
   381  }
   382  
   383  func (v *vppClient) Invoke(ctx context.Context, req govppapi.Message, reply govppapi.Message) error {
   384  	return v.conn.Invoke(ctx, req, reply)
   385  }
   386  
   387  func (v *vppClient) WatchEvent(ctx context.Context, event govppapi.Message) (govppapi.Watcher, error) {
   388  	return v.conn.WatchEvent(ctx, event)
   389  }
   390  
   391  func (v *vppClient) Version() vpp.Version {
   392  	return v.version
   393  }
   394  
   395  func (v *vppClient) BinapiVersion() vpp.Version {
   396  	vppapiChan, err := v.conn.NewAPIChannel()
   397  	if err != nil {
   398  		v.t.Fatalf("Can't create new API channel (to get binary API version) due to: %v", err)
   399  	}
   400  	binapiVersion, err := binapi.CompatibleVersion(vppapiChan)
   401  	if err != nil {
   402  		v.t.Fatalf("Can't get binary API version due to: %v", err)
   403  	}
   404  	return binapiVersion
   405  }
   406  
   407  func (v *vppClient) CheckCompatiblity(msgs ...govppapi.Message) error {
   408  	return v.ch.CheckCompatiblity(msgs...)
   409  }
   410  
   411  func (v *vppClient) Stats() govppapi.StatsProvider {
   412  	return v.stats
   413  }
   414  
   415  func (v *vppClient) IsPluginLoaded(plugin string) bool {
   416  	ctx := context.Background()
   417  	plugins, err := v.vpp.GetPlugins(ctx)
   418  	if err != nil {
   419  		v.t.Fatalf("GetPlugins failed: %v", plugins)
   420  	}
   421  	for _, p := range plugins {
   422  		if p.Name == plugin {
   423  			return true
   424  		}
   425  	}
   426  	return false
   427  }
   428  
   429  func (v *vppClient) OnReconnect(h func()) {
   430  	// no-op
   431  }