github.phpd.cn/cilium/cilium@v1.6.12/test/helpers/node.go (about)

     1  // Copyright 2017 Authors of Cilium
     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 helpers
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/cilium/cilium/test/config"
    28  	ginkgoext "github.com/cilium/cilium/test/ginkgo-ext"
    29  
    30  	"github.com/sirupsen/logrus"
    31  	"golang.org/x/crypto/ssh"
    32  )
    33  
    34  var (
    35  	//SSHMetaLogs is a buffer where all commands sent over ssh are saved.
    36  	SSHMetaLogs = ginkgoext.NewWriter(new(Buffer))
    37  )
    38  
    39  // SSHMeta contains metadata to SSH into a remote location to run tests
    40  type SSHMeta struct {
    41  	sshClient *SSHClient
    42  	env       []string
    43  	rawConfig []byte
    44  	nodeName  string
    45  	logger    *logrus.Entry
    46  }
    47  
    48  // CreateSSHMeta returns an SSHMeta with the specified host, port, and user, as
    49  // well as an according SSHClient.
    50  func CreateSSHMeta(host string, port int, user string) *SSHMeta {
    51  	return &SSHMeta{
    52  		sshClient: GetSSHClient(host, port, user),
    53  	}
    54  }
    55  
    56  // IsLocal returns true if commands are executed on the Ginkgo host
    57  func (s *SSHMeta) IsLocal() bool {
    58  	return false
    59  }
    60  
    61  func (s *SSHMeta) String() string {
    62  	return fmt.Sprintf("environment: %s, SSHClient: %s", s.env, s.sshClient.String())
    63  
    64  }
    65  
    66  // CloseSSHClient closes all of the connections made by the SSH Client for this
    67  // SSHMeta.
    68  func (s *SSHMeta) CloseSSHClient() {
    69  	if s.sshClient == nil || s.sshClient.client == nil {
    70  		log.Error("SSH client is nil; cannot close")
    71  	}
    72  	if err := s.sshClient.client.Close(); err != nil {
    73  		log.WithError(err).Error("error closing SSH client")
    74  	}
    75  }
    76  
    77  // GetVagrantSSHMeta returns a SSHMeta initialized based on the provided
    78  // SSH-config target.
    79  func GetVagrantSSHMeta(vmName string) *SSHMeta {
    80  	config, err := GetVagrantSSHMetadata(vmName)
    81  	if err != nil {
    82  		return nil
    83  	}
    84  
    85  	log.Debugf("generated SSHConfig for node %s", vmName)
    86  	nodes, err := ImportSSHconfig(config)
    87  	if err != nil {
    88  		log.WithError(err).Error("Error importing ssh config")
    89  		return nil
    90  	}
    91  	var node *SSHConfig
    92  	log.Debugf("done importing ssh config")
    93  	for name := range nodes {
    94  		if strings.HasPrefix(name, vmName) {
    95  			node = nodes[name]
    96  			break
    97  		}
    98  	}
    99  	if node == nil {
   100  		log.Errorf("Node %s not found in ssh config", vmName)
   101  		return nil
   102  	}
   103  	sshMeta := &SSHMeta{
   104  		sshClient: node.GetSSHClient(),
   105  		rawConfig: config,
   106  		nodeName:  vmName,
   107  	}
   108  
   109  	sshMeta.setBasePath()
   110  	return sshMeta
   111  }
   112  
   113  // setBasePath if the SSHConfig is defined we set the BasePath to the GOPATH,
   114  // from golang 1.8 GOPATH is by default $HOME/go so we also check that.
   115  func (s *SSHMeta) setBasePath() {
   116  	if config.CiliumTestConfig.SSHConfig == "" {
   117  		return
   118  	}
   119  
   120  	gopath := s.Exec("echo $GOPATH").SingleOut()
   121  	if gopath != "" {
   122  		BasePath = filepath.Join(gopath, CiliumPath)
   123  		return
   124  	}
   125  
   126  	home := s.Exec("echo $HOME").SingleOut()
   127  	if home == "" {
   128  		return
   129  	}
   130  
   131  	BasePath = filepath.Join(home, "go", CiliumPath)
   132  	return
   133  }
   134  
   135  // ExecuteContext executes the given `cmd` and writes the cmd's stdout and
   136  // stderr into the given io.Writers.
   137  // Returns an error if context Deadline() is reached or if there was an error
   138  // executing the command.
   139  func (s *SSHMeta) ExecuteContext(ctx context.Context, cmd string, stdout io.Writer, stderr io.Writer) error {
   140  	if stdout == nil {
   141  		stdout = os.Stdout
   142  	}
   143  
   144  	if stderr == nil {
   145  		stderr = os.Stderr
   146  	}
   147  	fmt.Fprintln(SSHMetaLogs, cmd)
   148  	command := &SSHCommand{
   149  		Path:   cmd,
   150  		Stdin:  os.Stdin,
   151  		Stdout: stdout,
   152  		Stderr: stderr,
   153  	}
   154  	return s.sshClient.RunCommandContext(ctx, command)
   155  }
   156  
   157  // ExecWithSudo returns the result of executing the provided cmd via SSH using
   158  // sudo.
   159  func (s *SSHMeta) ExecWithSudo(cmd string, options ...ExecOptions) *CmdRes {
   160  	command := fmt.Sprintf("sudo %s", cmd)
   161  	return s.Exec(command, options...)
   162  }
   163  
   164  // ExecOptions options to execute Exec and ExecWithContext
   165  type ExecOptions struct {
   166  	SkipLog bool
   167  }
   168  
   169  // Exec returns the results of executing the provided cmd via SSH.
   170  func (s *SSHMeta) Exec(cmd string, options ...ExecOptions) *CmdRes {
   171  	// Bound all command executions to be at most the timeout used by the CI
   172  	// so that commands do not block forever.
   173  	ctx, cancel := context.WithTimeout(context.Background(), HelperTimeout)
   174  	defer cancel()
   175  	return s.ExecContext(ctx, cmd, options...)
   176  }
   177  
   178  // ExecShort runs command with the provided options. It will take up to
   179  // ShortCommandTimeout seconds to run the command before it times out.
   180  func (s *SSHMeta) ExecShort(cmd string, options ...ExecOptions) *CmdRes {
   181  	ctx, cancel := context.WithTimeout(context.Background(), ShortCommandTimeout)
   182  	defer cancel()
   183  	return s.ExecContext(ctx, cmd, options...)
   184  }
   185  
   186  // ExecMiddle runs command with the provided options. It will take up to
   187  // MidCommandTimeout seconds to run the command before it times out.
   188  func (s *SSHMeta) ExecMiddle(cmd string, options ...ExecOptions) *CmdRes {
   189  	ctx, cancel := context.WithTimeout(context.Background(), MidCommandTimeout)
   190  	defer cancel()
   191  	return s.ExecContext(ctx, cmd, options...)
   192  }
   193  
   194  // ExecContextShort is a wrapper around ExecContext which creates a child
   195  // context with a timeout of ShortCommandTimeout.
   196  func (s *SSHMeta) ExecContextShort(ctx context.Context, cmd string, options ...ExecOptions) *CmdRes {
   197  	shortCtx, cancel := context.WithTimeout(ctx, ShortCommandTimeout)
   198  	defer cancel()
   199  	return s.ExecContext(shortCtx, cmd, options...)
   200  }
   201  
   202  // ExecContext returns the results of executing the provided cmd via SSH.
   203  func (s *SSHMeta) ExecContext(ctx context.Context, cmd string, options ...ExecOptions) *CmdRes {
   204  	var ops ExecOptions
   205  	if len(options) > 0 {
   206  		ops = options[0]
   207  	}
   208  
   209  	log.Debugf("running command: %s", cmd)
   210  	stdout := new(Buffer)
   211  	stderr := new(Buffer)
   212  	start := time.Now()
   213  	err := s.ExecuteContext(ctx, cmd, stdout, stderr)
   214  
   215  	res := CmdRes{
   216  		cmd:      cmd,
   217  		stdout:   stdout,
   218  		stderr:   stderr,
   219  		success:  true, // this may be toggled when err != nil below
   220  		duration: time.Since(start),
   221  	}
   222  
   223  	if err != nil {
   224  		res.success = false
   225  		// Set error code to 1 in case that it's another error to see that the
   226  		// command failed. If the default value (0) indicates that command
   227  		// works but it was not executed at all.
   228  		res.exitcode = 1
   229  		exiterr, isExitError := err.(*ssh.ExitError)
   230  		if isExitError {
   231  			// Set res's exitcode if the error is an ExitError
   232  			res.exitcode = exiterr.Waitmsg.ExitStatus()
   233  		} else {
   234  			// Log other error types. They are likely from SSH or the network
   235  			log.WithError(err).Errorf("Error executing command '%s'", cmd)
   236  			res.err = err
   237  		}
   238  	}
   239  
   240  	res.SendToLog(ops.SkipLog)
   241  	return &res
   242  }
   243  
   244  // GetCopy returns a copy of SSHMeta, useful for parallel requests
   245  func (s *SSHMeta) GetCopy() *SSHMeta {
   246  	nodes, err := ImportSSHconfig(s.rawConfig)
   247  	if err != nil {
   248  		log.WithError(err).Error("while importing ssh config for meta copy")
   249  		return nil
   250  	}
   251  
   252  	config := nodes[s.nodeName]
   253  	if config == nil {
   254  		log.Errorf("no node %s in imported config", s.nodeName)
   255  		return nil
   256  	}
   257  
   258  	copy := &SSHMeta{
   259  		sshClient: config.GetSSHClient(),
   260  		rawConfig: s.rawConfig,
   261  		nodeName:  s.nodeName,
   262  	}
   263  
   264  	return copy
   265  }
   266  
   267  // ExecInBackground returns the results of running cmd via SSH in the specified
   268  // context. The command will be executed in the background until context.Context
   269  // is canceled or the command has finish its execution.
   270  func (s *SSHMeta) ExecInBackground(ctx context.Context, cmd string, options ...ExecOptions) *CmdRes {
   271  	if ctx == nil {
   272  		panic("no context provided")
   273  	}
   274  
   275  	var ops ExecOptions
   276  	if len(options) > 0 {
   277  		ops = options[0]
   278  	}
   279  
   280  	fmt.Fprintln(SSHMetaLogs, cmd)
   281  	stdout := new(Buffer)
   282  	stderr := new(Buffer)
   283  
   284  	command := &SSHCommand{
   285  		Path:   cmd,
   286  		Stdin:  os.Stdin,
   287  		Stdout: stdout,
   288  		Stderr: stderr,
   289  	}
   290  	var wg sync.WaitGroup
   291  	res := &CmdRes{
   292  		cmd:     cmd,
   293  		stdout:  stdout,
   294  		stderr:  stderr,
   295  		success: false,
   296  		wg:      &wg,
   297  	}
   298  
   299  	res.wg.Add(1)
   300  	go func(res *CmdRes) {
   301  		defer res.wg.Done()
   302  		start := time.Now()
   303  		err := s.sshClient.RunCommandInBackground(ctx, command)
   304  		if err != nil {
   305  			exiterr, isExitError := err.(*ssh.ExitError)
   306  			if isExitError {
   307  				res.exitcode = exiterr.Waitmsg.ExitStatus()
   308  				// Set success as true if SIGINT signal was sent to command
   309  				if res.exitcode == 130 {
   310  					res.success = true
   311  				}
   312  			}
   313  		} else {
   314  			res.success = true
   315  			res.exitcode = 0
   316  		}
   317  		res.duration = time.Since(start)
   318  		res.SendToLog(ops.SkipLog)
   319  	}(res)
   320  
   321  	return res
   322  }