github.com/rohankumardubey/nomad@v0.11.8/e2e/framework/provisioning/ssh_runner.go (about)

     1  package provisioning
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  )
    14  
    15  // SSHRunner is a ProvisioningRunner that deploys via ssh.
    16  // Terraform does all of this more elegantly and portably in its
    17  // ssh communicator, but by shelling out we avoid pulling in TF's as
    18  // a Nomad dependency, and avoid some long-standing issues with
    19  // connections to Windows servers. The tradeoff is losing portability
    20  // but in practice we're always going to run this from a Unixish
    21  // machine.
    22  type SSHRunner struct {
    23  	Key  string // `json:"key"`
    24  	User string // `json:"user"`
    25  	Host string // `json:"host"`
    26  	Port int    // `json:"port"`
    27  
    28  	// none of these are available at time of construction, but
    29  	// should be populated in Open().
    30  	t               *testing.T
    31  	controlSockPath string
    32  	ctx             context.Context
    33  	cancelFunc      context.CancelFunc
    34  	copyMethod      func(*SSHRunner, string, string) error
    35  	muxWait         chan struct{}
    36  }
    37  
    38  // Open establishes the ssh connection. We keep this connection open
    39  // so that we can multiplex subsequent ssh connections.
    40  func (runner *SSHRunner) Open(t *testing.T) error {
    41  	runner.t = t
    42  	runner.Logf("opening connection to %s", runner.Host)
    43  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
    44  	runner.ctx = ctx
    45  	runner.cancelFunc = cancel
    46  	runner.muxWait = make(chan struct{})
    47  
    48  	home, _ := os.UserHomeDir()
    49  	runner.controlSockPath = filepath.Join(
    50  		home, ".ssh",
    51  		fmt.Sprintf("ssh-control-%s-%d.sock", runner.Host, os.Getpid()))
    52  
    53  	cmd := exec.CommandContext(ctx,
    54  		"ssh",
    55  		"-M", "-S", runner.controlSockPath,
    56  		"-o", "StrictHostKeyChecking=no", // we're those terrible cloud devs
    57  		"-o", "UserKnownHostsFile=/dev/null",
    58  		"-o", "LogLevel=ERROR",
    59  		"-o", "ConnectTimeout=60", // give the target a while to come up
    60  		"-i", runner.Key,
    61  		"-p", fmt.Sprintf("%v", runner.Port),
    62  		fmt.Sprintf("%s@%s", runner.User, runner.Host),
    63  	)
    64  
    65  	go func() {
    66  		// will block until command completes, we cancel, or timeout.
    67  		// there's no point in returning the error here as we only
    68  		// hit it when we're done and Windows unfortunately tends to
    69  		// return 1 even when the script is complete.
    70  		cmd.Run()
    71  		runner.muxWait <- struct{}{}
    72  	}()
    73  	return nil
    74  }
    75  
    76  func (runner *SSHRunner) Run(script string) error {
    77  	commands := strings.Split(strings.TrimSpace(script), "\n")
    78  	for _, command := range commands {
    79  		err := runner.run(strings.TrimSpace(command))
    80  		if err != nil {
    81  			runner.cancelFunc()
    82  			return err
    83  		}
    84  	}
    85  	return nil
    86  }
    87  
    88  func (runner *SSHRunner) run(command string) error {
    89  	if runner.controlSockPath == "" {
    90  		return fmt.Errorf("Run failed: you need to call Open() first")
    91  	}
    92  	runner.Logf("running '%s'", command)
    93  	cmd := exec.CommandContext(runner.ctx,
    94  		"ssh",
    95  		"-S", runner.controlSockPath,
    96  		"-o", "StrictHostKeyChecking=no",
    97  		"-o", "UserKnownHostsFile=/dev/null",
    98  		"-o", "LogLevel=ERROR",
    99  		"-i", runner.Key,
   100  		"-p", fmt.Sprintf("%v", runner.Port),
   101  		fmt.Sprintf("%s@%s", runner.User, runner.Host),
   102  		command)
   103  
   104  	stdoutStderr, err := cmd.CombinedOutput()
   105  	if err != nil && err != context.Canceled {
   106  		runner.LogErrOutput(string(stdoutStderr))
   107  		return err
   108  	}
   109  	runner.LogOutput(string(stdoutStderr))
   110  	return nil
   111  }
   112  
   113  // Copy uploads the local path to the remote path. We call into
   114  // different copy methods for Linux vs Windows because their path
   115  // semantics are slightly different and the typical ssh users have
   116  // different permissions.
   117  func (runner *SSHRunner) Copy(local, remote string) error {
   118  	return runner.copyMethod(runner, local, remote)
   119  }
   120  
   121  // TODO: would be nice to set file owner/mode here
   122  func copyLinux(runner *SSHRunner, local, remote string) error {
   123  	t := runner.t
   124  	runner.Logf("copying '%s' to '%s'", local, remote)
   125  	remoteDir, remoteFileName := filepath.Split(remote)
   126  
   127  	// we stage to /tmp so that we can handle root-owned files
   128  	tempPath := fmt.Sprintf("/tmp/%s", remoteFileName)
   129  
   130  	cmd := exec.CommandContext(runner.ctx,
   131  		"scp", "-r",
   132  		"-o", fmt.Sprintf("ControlPath=%s", runner.controlSockPath),
   133  		"-o", "StrictHostKeyChecking=no",
   134  		"-o", "UserKnownHostsFile=/dev/null",
   135  		"-o", "LogLevel=ERROR",
   136  		"-i", runner.Key,
   137  		"-P", fmt.Sprintf("%v", runner.Port),
   138  		local,
   139  		fmt.Sprintf("%s@%s:%s", runner.User, runner.Host, tempPath))
   140  
   141  	stdoutStderr, err := cmd.CombinedOutput()
   142  	if err != nil && err != context.Canceled {
   143  		runner.LogErrOutput(string(stdoutStderr))
   144  		runner.cancelFunc()
   145  		return err
   146  	}
   147  
   148  	fi, err := os.Stat(local)
   149  	if err != nil {
   150  		t.Fatalf("could not read '%s'", local)
   151  	}
   152  	if fi.IsDir() {
   153  		// this is a little inefficient but it lets us merge the contents of
   154  		// a bundled directory with existing directories
   155  		err = runner.Run(
   156  			fmt.Sprintf("sudo mkdir -p %s; sudo cp -R %s %s; sudo rm -r %s",
   157  				remote, tempPath, remoteDir, tempPath))
   158  	} else {
   159  		err = runner.run(fmt.Sprintf("sudo mv %s %s", tempPath, remoteDir))
   160  	}
   161  	return err
   162  }
   163  
   164  // staging to Windows tempdirs is a little messier, but "fortunately"
   165  // nobody seems to complain about connecting via ssh as Administrator on
   166  // Windows so we can just bypass the problem.
   167  func copyWindows(runner *SSHRunner, local, remote string) error {
   168  	runner.Logf("copying '%s' to '%s'", local, remote)
   169  	remoteDir, _ := filepath.Split(remote)
   170  	fi, err := os.Stat(local)
   171  	if err != nil {
   172  		runner.t.Fatalf("could not read '%s'", local)
   173  	}
   174  	remotePath := remote
   175  	if fi.IsDir() {
   176  		remotePath = remoteDir
   177  	}
   178  	cmd := exec.CommandContext(runner.ctx,
   179  		"scp", "-r",
   180  		"-o", fmt.Sprintf("ControlPath=%s", runner.controlSockPath),
   181  		"-o", "StrictHostKeyChecking=no",
   182  		"-o", "UserKnownHostsFile=/dev/null",
   183  		"-o", "LogLevel=ERROR",
   184  		"-i", runner.Key,
   185  		"-P", fmt.Sprintf("%v", runner.Port),
   186  		local,
   187  		fmt.Sprintf("%s@%s:'%s'", runner.User, runner.Host, remotePath))
   188  
   189  	stdoutStderr, err := cmd.CombinedOutput()
   190  	if err != nil && err != context.Canceled {
   191  		runner.LogErrOutput(string(stdoutStderr))
   192  		runner.cancelFunc()
   193  		return err
   194  	}
   195  	return err
   196  }
   197  
   198  func (runner *SSHRunner) Close() {
   199  	runner.Log("closing connection")
   200  	runner.cancelFunc()
   201  	<-runner.muxWait
   202  }
   203  
   204  // 'go test -v' only emits logs after the entire test run is complete,
   205  // but that makes it much harder to debug hanging deployments. These
   206  // methods wrap the test logger or just emit directly w/ fmt.Print if
   207  // the '-v' flag was set.
   208  
   209  func (runner *SSHRunner) Log(args ...interface{}) {
   210  	if runner.t == nil {
   211  		log.Fatal("no t.Testing configured for SSHRunner")
   212  	}
   213  	if testing.Verbose() {
   214  		fmt.Printf("[" + runner.Host + "] ")
   215  		fmt.Println(args...)
   216  	} else {
   217  		runner.t.Log(args...)
   218  	}
   219  }
   220  
   221  func (runner *SSHRunner) Logf(format string, args ...interface{}) {
   222  	if runner.t == nil {
   223  		log.Fatal("no t.Testing configured for SSHRunner")
   224  	}
   225  	if testing.Verbose() {
   226  		fmt.Printf("["+runner.Host+"] "+format+"\n", args...)
   227  	} else {
   228  		runner.t.Logf("["+runner.Host+"] "+format, args...)
   229  	}
   230  }
   231  
   232  func (runner *SSHRunner) LogOutput(output string) {
   233  	if testing.Verbose() {
   234  		fmt.Println("\033[32m" + output + "\033[0m")
   235  	} else {
   236  		runner.t.Log(output)
   237  	}
   238  }
   239  
   240  func (runner *SSHRunner) LogErrOutput(output string) {
   241  	if testing.Verbose() {
   242  		fmt.Println("\033[31m" + output + "\033[0m")
   243  	} else {
   244  		runner.t.Log(output)
   245  	}
   246  }