go.ligato.io/vpp-agent/v3@v3.5.0/plugins/govppmux/plugin_impl_govppmux.go (about)

     1  //  Copyright (c) 2021 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 govppmux
    16  
    17  import (
    18  	"context"
    19  	"encoding/gob"
    20  	"fmt"
    21  	"os"
    22  	"path"
    23  	"reflect"
    24  	"sort"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/pkg/errors"
    30  	"go.fd.io/govpp/adapter"
    31  	govppapi "go.fd.io/govpp/api"
    32  	govpp "go.fd.io/govpp/core"
    33  	"go.fd.io/govpp/proxy"
    34  	"go.ligato.io/cn-infra/v2/datasync/resync"
    35  	"go.ligato.io/cn-infra/v2/health/statuscheck"
    36  	"go.ligato.io/cn-infra/v2/infra"
    37  	"go.ligato.io/cn-infra/v2/logging"
    38  	"go.ligato.io/cn-infra/v2/rpc/rest"
    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  	_ "go.ligato.io/vpp-agent/v3/plugins/govppmux/vppcalls/vpp2101"
    45  	_ "go.ligato.io/vpp-agent/v3/plugins/govppmux/vppcalls/vpp2106"
    46  	_ "go.ligato.io/vpp-agent/v3/plugins/govppmux/vppcalls/vpp2202"
    47  	_ "go.ligato.io/vpp-agent/v3/plugins/govppmux/vppcalls/vpp2210"
    48  )
    49  
    50  var (
    51  	disabledSocketClient = os.Getenv("GOVPPMUX_NOSOCK") != ""
    52  )
    53  
    54  // Plugin is the govppmux plugin implementation.
    55  type Plugin struct {
    56  	Deps
    57  
    58  	config *Config
    59  
    60  	vpeHandler vppcalls.VppCoreAPI
    61  
    62  	binapiVersion vpp.Version
    63  	vppAdapter    adapter.VppAPI
    64  	vppConn       *govpp.Connection
    65  	vppConChan    chan govpp.ConnectionEvent
    66  	lastConnErr   error
    67  	vppapiChan    govppapi.Channel
    68  	lastPID       uint32
    69  
    70  	onnConnectMu sync.Mutex
    71  	onConnects   []func()
    72  
    73  	statsMu      sync.Mutex
    74  	statsAdapter adapter.StatsAPI
    75  	statsConn    *govpp.StatsConnection
    76  
    77  	proxy *proxy.Server
    78  
    79  	// infoMu synchonizes access to fields
    80  	// vppInfo and lastEvent
    81  	infoMu    sync.Mutex
    82  	vppInfo   VPPInfo
    83  	lastEvent govpp.ConnectionEvent
    84  
    85  	cancel context.CancelFunc
    86  	wg     sync.WaitGroup
    87  }
    88  
    89  // Deps defines dependencies for the govppmux plugin.
    90  type Deps struct {
    91  	infra.PluginDeps
    92  	HTTPHandlers rest.HTTPHandlers
    93  	StatusCheck  statuscheck.PluginStatusWriter
    94  	Resync       *resync.Plugin
    95  }
    96  
    97  // Init is the entry point called by Agent Core. A single binary-API connection to VPP is established.
    98  func (p *Plugin) Init() (err error) {
    99  	if p.config, err = p.loadConfig(); err != nil {
   100  		return err
   101  	}
   102  	p.Log.Debugf("config: %+v", p.config)
   103  
   104  	// set GoVPP config
   105  	govpp.HealthCheckProbeInterval = p.config.HealthCheckProbeInterval
   106  	govpp.HealthCheckReplyTimeout = p.config.HealthCheckReplyTimeout
   107  	govpp.HealthCheckThreshold = p.config.HealthCheckThreshold
   108  	govpp.DefaultReplyTimeout = p.config.ReplyTimeout
   109  
   110  	var address string
   111  	useShm := disabledSocketClient || p.config.ConnectViaShm || p.config.ShmPrefix != ""
   112  	if useShm {
   113  		address = p.config.ShmPrefix
   114  	} else {
   115  		address = p.config.BinAPISocketPath
   116  	}
   117  
   118  	p.Log.Debugf("found %d registered VPP handlers", len(vpp.GetHandlers()))
   119  	for name, handler := range vpp.GetHandlers() {
   120  		versions := handler.Versions()
   121  		p.Log.Debugf("- handler: %-10s has %d versions: %v", name, len(versions), versions)
   122  	}
   123  
   124  	// FIXME: this is a hack for GoVPP bug when register of the same message(same CRC and name) but different
   125  	//  VPP version overwrites the already registered message from one VPP version (map key is only CRC+name
   126  	//  and that didn't change with VPP version, but generated binapi generated 2 different go types for it).
   127  	//  Similar fix exists also for integration tests.
   128  	if err := p.hackForBugInGoVPPMessageCache(address, useShm); err != nil {
   129  		return errors.Errorf("can't apply hack fixing bug in GoVPP "+
   130  			"regarding stream's message type resolving: %v", err)
   131  	}
   132  
   133  	// TODO: Async connect & automatic reconnect support is not yet implemented in the agent,
   134  	// so synchronously wait until connected to VPP.
   135  	startTime := time.Now()
   136  	p.Log.Debugf("connecting to VPP..")
   137  
   138  	p.vppAdapter = NewVppAdapter(address, useShm)
   139  	p.vppConn, p.vppConChan, err = govpp.AsyncConnect(p.vppAdapter, p.config.RetryConnectCount, p.config.RetryConnectTimeout)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	if err := p.waitForConnectionEvent(p.vppConChan); err != nil {
   144  		return err
   145  	}
   146  	took := time.Since(startTime)
   147  	p.Log.Debugf("connection to VPP established (took %s)", took.Round(time.Millisecond))
   148  
   149  	if err := p.updateVPPInfo(); err != nil {
   150  		return errors.WithMessage(err, "retrieving VPP info failed")
   151  	}
   152  
   153  	// Connect to VPP status socket
   154  	var statsSocket string
   155  	if p.config.StatsSocketPath != "" {
   156  		statsSocket = p.config.StatsSocketPath
   157  	} else {
   158  		statsSocket = adapter.DefaultStatsSocket
   159  	}
   160  	statsAdapter := NewStatsAdapter(statsSocket)
   161  	if p.statsConn, err = govpp.ConnectStats(statsAdapter); err != nil {
   162  		p.Log.Warnf("Unable to connect to the VPP statistics socket, %v", err)
   163  		p.statsAdapter = nil
   164  	}
   165  
   166  	if p.config.ProxyEnabled {
   167  		// register binapi messages to gob package (required for proxy)
   168  		msgList := binapi.Versions[p.binapiVersion]
   169  		for _, msg := range msgList.AllMessages() {
   170  			gob.Register(msg)
   171  		}
   172  		err := p.startProxy(NewVppAdapter(address, useShm), NewStatsAdapter(statsSocket))
   173  		if err != nil {
   174  			p.Log.Warnf("VPP proxy failed to start: %v", err)
   175  		} else {
   176  			p.Log.Infof("VPP proxy ready")
   177  		}
   178  	}
   179  
   180  	// register REST API handlers
   181  	p.registerHandlers(p.HTTPHandlers)
   182  
   183  	return nil
   184  }
   185  
   186  // waitForConnectionEvent waits for Connected event from govpp
   187  func (p *Plugin) waitForConnectionEvent(vppConChan chan govpp.ConnectionEvent) error {
   188  	for {
   189  		event, ok := <-vppConChan
   190  		if !ok {
   191  			return errors.Errorf("VPP connection state channel closed")
   192  		}
   193  		if event.State == govpp.Connected {
   194  			break
   195  		} else if event.State == govpp.Failed || event.State == govpp.Disconnected {
   196  			return errors.Errorf("unable to establish connection to VPP (%v)", event.Error)
   197  		} else {
   198  			p.Log.Debugf("VPP connection state: %+v", event)
   199  		}
   200  	}
   201  	return nil
   202  }
   203  
   204  func (p *Plugin) hackForBugInGoVPPMessageCache(address string, useShm bool) error {
   205  	// connect to VPP
   206  	startTime := time.Now()
   207  	vppAdapter := NewVppAdapter(address, useShm)
   208  	vppConn, vppConChan, err := govpp.AsyncConnect(vppAdapter, p.config.RetryConnectCount, p.config.RetryConnectTimeout)
   209  	if err != nil {
   210  		return err
   211  	}
   212  	if err := p.waitForConnectionEvent(vppConChan); err != nil {
   213  		return err
   214  	}
   215  	took := time.Since(startTime)
   216  	p.Log.Debugf("first connection to VPP established (took %s)", took.Round(time.Millisecond))
   217  
   218  	if vppConn == nil {
   219  		return fmt.Errorf("VPP connection is nil")
   220  	}
   221  
   222  	// detect binary API version
   223  	vppapiChan, err := vppConn.NewAPIChannel()
   224  	if err != nil {
   225  		return err
   226  	}
   227  	binapiVersion, err := binapi.CompatibleVersion(vppapiChan)
   228  	if err != nil {
   229  		return err
   230  	}
   231  
   232  	// overwrite messages with messages from correct VPP version
   233  	for _, msg := range binapi.Versions[binapiVersion].AllMessages() {
   234  		reRegisterMessage(msg)
   235  	}
   236  
   237  	// disconnect from VPP (GoVPP is caching the messages that we want to override
   238  	// by first connection to VPP -> we must disconnect and reconnect later again)
   239  	vppConn.Disconnect()
   240  
   241  	return nil
   242  }
   243  
   244  // reRegisterMessage overwrites the original registration of Messages in GoVPP with new message registration.
   245  func reRegisterMessage(x govppapi.Message) {
   246  	typ := reflect.TypeOf(x)
   247  	namecrc := x.GetMessageName() + "_" + x.GetCrcString()
   248  	binapiPath := path.Dir(reflect.TypeOf(x).Elem().PkgPath())
   249  	govppapi.GetRegisteredMessages()[binapiPath][namecrc] = x
   250  	govppapi.GetRegisteredMessageTypes()[binapiPath][typ] = namecrc
   251  }
   252  
   253  // AfterInit reports status check.
   254  func (p *Plugin) AfterInit() error {
   255  	// Register providing status reports (push mode)
   256  	p.StatusCheck.Register(p.PluginName, nil)
   257  	p.StatusCheck.ReportStateChange(p.PluginName, statuscheck.OK, nil)
   258  
   259  	var ctx context.Context
   260  	ctx, p.cancel = context.WithCancel(context.Background())
   261  
   262  	p.wg.Add(1)
   263  	go p.handleVPPConnectionEvents(ctx)
   264  
   265  	return nil
   266  }
   267  
   268  // Close cleans up the resources allocated by the govppmux plugin.
   269  func (p *Plugin) Close() error {
   270  	p.cancel()
   271  	p.wg.Wait()
   272  
   273  	defer func() {
   274  		if p.vppConn != nil {
   275  			p.vppConn.Disconnect()
   276  		}
   277  		if p.statsAdapter != nil {
   278  			if err := p.statsAdapter.Disconnect(); err != nil {
   279  				p.Log.Errorf("VPP statistics socket adapter disconnect error: %v", err)
   280  			}
   281  		}
   282  	}()
   283  
   284  	if p.proxy != nil {
   285  		p.proxy.DisconnectBinapi()
   286  		p.proxy.DisconnectStats()
   287  	}
   288  
   289  	return nil
   290  }
   291  
   292  func (p *Plugin) Version() vpp.Version {
   293  	return p.binapiVersion
   294  }
   295  
   296  func (p *Plugin) CheckCompatiblity(msgs ...govppapi.Message) error {
   297  	p.infoMu.Lock()
   298  	defer p.infoMu.Unlock()
   299  	if p.vppapiChan == nil {
   300  		apiChan, err := p.vppConn.NewAPIChannel()
   301  		if err != nil {
   302  			return err
   303  		}
   304  		p.vppapiChan = apiChan
   305  	}
   306  	return p.vppapiChan.CheckCompatiblity(msgs...)
   307  }
   308  
   309  func (p *Plugin) Stats() govppapi.StatsProvider {
   310  	if p.statsConn == nil {
   311  		return nil
   312  	}
   313  	return p
   314  }
   315  
   316  func (p *Plugin) BinapiVersion() vpp.Version {
   317  	return p.binapiVersion
   318  }
   319  
   320  // VPPInfo returns information about VPP session.
   321  func (p *Plugin) VPPInfo() VPPInfo {
   322  	p.infoMu.Lock()
   323  	defer p.infoMu.Unlock()
   324  	return p.vppInfo
   325  }
   326  
   327  // IsPluginLoaded returns true if plugin is loaded.
   328  func (p *Plugin) IsPluginLoaded(plugin string) bool {
   329  	p.infoMu.Lock()
   330  	defer p.infoMu.Unlock()
   331  	for _, p := range p.vppInfo.Plugins {
   332  		if p.Name == plugin {
   333  			return true
   334  		}
   335  	}
   336  	return false
   337  }
   338  
   339  func (p *Plugin) updateVPPInfo() (err error) {
   340  	if p.vppConn == nil {
   341  		return fmt.Errorf("VPP connection is nil")
   342  	}
   343  
   344  	p.vppapiChan, err = p.vppConn.NewAPIChannel()
   345  	if err != nil {
   346  		return err
   347  	}
   348  	p.binapiVersion, err = binapi.CompatibleVersion(p.vppapiChan)
   349  	if err != nil {
   350  		return err
   351  	}
   352  
   353  	p.vpeHandler, err = vppcalls.NewHandler(p)
   354  	if err != nil {
   355  		return errors.New("no compatible VPP handler found")
   356  	}
   357  
   358  	ctx := context.TODO()
   359  
   360  	version, err := p.vpeHandler.RunCli(ctx, "show version verbose")
   361  	if err != nil {
   362  		p.Log.Warnf("RunCli error: %v", err)
   363  	} else {
   364  		p.Log.Debugf("vpp# show version verbose\n%s", version)
   365  	}
   366  	cmdline, err := p.vpeHandler.RunCli(ctx, "show version cmdline")
   367  	if err != nil {
   368  		p.Log.Warnf("RunCli error: %v", err)
   369  	} else {
   370  		out := strings.Replace(cmdline, "\n", "", -1)
   371  		p.Log.Debugf("vpp# show version cmdline:\n%s", out)
   372  	}
   373  
   374  	ver, err := p.vpeHandler.GetVersion(ctx)
   375  	if err != nil {
   376  		return err
   377  	}
   378  	session, err := p.vpeHandler.GetSession(ctx)
   379  	if err != nil {
   380  		return err
   381  	}
   382  	p.Log.WithFields(logging.Fields{
   383  		"PID":      session.PID,
   384  		"ClientID": session.ClientIdx,
   385  	}).Infof("VPP version: %v", ver.Version)
   386  
   387  	modules, err := p.vpeHandler.GetModules(ctx)
   388  	if err != nil {
   389  		return err
   390  	}
   391  	p.Log.Debugf("VPP has %d core modules: %v", len(modules), modules)
   392  
   393  	plugins, err := p.vpeHandler.GetPlugins(ctx)
   394  	if err != nil {
   395  		return err
   396  	}
   397  	// sort plugins by name
   398  	sort.Slice(plugins, func(i, j int) bool { return plugins[i].Name < plugins[j].Name })
   399  
   400  	p.Log.Debugf("VPP loaded %d plugins", len(plugins))
   401  	for _, plugin := range plugins {
   402  		p.Log.Debugf(" - plugin: %v", plugin)
   403  	}
   404  
   405  	p.infoMu.Lock()
   406  	p.vppInfo = VPPInfo{
   407  		Connected:   true,
   408  		VersionInfo: *ver,
   409  		SessionInfo: *session,
   410  		Plugins:     plugins,
   411  	}
   412  	p.infoMu.Unlock()
   413  
   414  	if p.lastPID != 0 && p.lastPID != session.PID {
   415  		p.Log.Warnf("VPP has restarted (previous PID: %d)", p.lastPID)
   416  		p.onConnect()
   417  	}
   418  	p.lastPID = session.PID
   419  
   420  	return nil
   421  }
   422  
   423  func (p *Plugin) OnReconnect(fn func()) {
   424  	p.onnConnectMu.Lock()
   425  	defer p.onnConnectMu.Unlock()
   426  	p.onConnects = append(p.onConnects, fn)
   427  }
   428  
   429  func (p *Plugin) onConnect() {
   430  	p.onnConnectMu.Lock()
   431  	defer p.onnConnectMu.Unlock()
   432  	for _, fn := range p.onConnects {
   433  		fn()
   434  	}
   435  }
   436  
   437  // handleVPPConnectionEvents handles VPP connection events.
   438  func (p *Plugin) handleVPPConnectionEvents(ctx context.Context) {
   439  	defer p.wg.Done()
   440  
   441  	for {
   442  		select {
   443  		case event, ok := <-p.vppConChan:
   444  			if !ok {
   445  				p.lastConnErr = errors.Errorf("VPP connection state channel closed")
   446  				p.StatusCheck.ReportStateChange(p.PluginName, statuscheck.Error, p.lastConnErr)
   447  				return
   448  			}
   449  
   450  			p.Log.Debugf("VPP connection state changed: %+v", event)
   451  
   452  			if event.State == govpp.Connected {
   453  				if err := p.updateVPPInfo(); err != nil {
   454  					p.Log.Errorf("updating VPP info failed: %v", err)
   455  				}
   456  
   457  				if p.config.ReconnectResync && p.lastConnErr != nil {
   458  					p.Log.Info("Starting resync after VPP reconnect")
   459  					if p.Resync != nil {
   460  						p.Resync.DoResync()
   461  						p.lastConnErr = nil
   462  					} else {
   463  						p.Log.Warn("Expected resync after VPP reconnect could not start because of missing Resync plugin")
   464  					}
   465  				}
   466  				p.StatusCheck.ReportStateChange(p.PluginName, statuscheck.OK, nil)
   467  			} else if event.State == govpp.Failed || event.State == govpp.Disconnected {
   468  				p.infoMu.Lock()
   469  				p.vppInfo.Connected = false
   470  				p.infoMu.Unlock()
   471  
   472  				p.lastConnErr = errors.Errorf("VPP connection lost (event: %+v)", event)
   473  				p.StatusCheck.ReportStateChange(p.PluginName, statuscheck.Error, p.lastConnErr)
   474  
   475  				// TODO: fix reconnecting after reaching maximum reconnect attempts
   476  				//		current implementation wont work with already created govpp channels
   477  				// keep reconnecting
   478  				/*if event.State == govpp.Failed {
   479  					p.vppConn, p.vppConChan, _ = govpp.AsyncConnect(p.vppAdapter, p.config.RetryConnectCount, p.config.RetryConnectTimeout)
   480  				}*/
   481  			} else if event.State == govpp.NotResponding {
   482  				p.infoMu.Lock()
   483  				p.vppInfo.Connected = false
   484  				p.infoMu.Unlock()
   485  
   486  				p.lastConnErr = errors.Errorf("VPP is not responding (event: %+v)", event)
   487  				p.StatusCheck.ReportStateChange(p.PluginName, statuscheck.Error, p.lastConnErr)
   488  			} else {
   489  				p.Log.Warnf("unknown VPP connection state: %+v", event)
   490  			}
   491  
   492  			p.infoMu.Lock()
   493  			p.lastEvent = event
   494  			p.infoMu.Unlock()
   495  
   496  		case <-ctx.Done():
   497  			return
   498  		}
   499  	}
   500  }
   501  
   502  func (p *Plugin) startProxy(vppapi adapter.VppAPI, statsapi adapter.StatsAPI) (err error) {
   503  	p.Log.Debugf("starting VPP proxy")
   504  
   505  	p.proxy, err = proxy.NewServer()
   506  	if err != nil {
   507  		return errors.Wrap(err, "creating proxy failed")
   508  	}
   509  	if err = p.proxy.ConnectBinapi(vppapi); err != nil {
   510  		return errors.Wrap(err, "connecting binapi for proxy failed")
   511  	}
   512  	if err = p.proxy.ConnectStats(statsapi); err != nil {
   513  		return errors.Wrap(err, "connecting stats for proxy failed")
   514  	}
   515  	return nil
   516  }