github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/commands/ssh_unix_test.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Copyright 2014 Cloudbase Solutions SRL
     3  // Licensed under the AGPLv3, see LICENCE file for details.
     4  
     5  // +build !windows
     6  
     7  package commands
     8  
     9  import (
    10  	"fmt"
    11  	"reflect"
    12  
    13  	"github.com/juju/cmd/cmdtesting"
    14  	"github.com/juju/testing"
    15  	jc "github.com/juju/testing/checkers"
    16  	gc "gopkg.in/check.v1"
    17  
    18  	"github.com/juju/juju/apiserver"
    19  	jujussh "github.com/juju/juju/network/ssh"
    20  )
    21  
    22  type SSHSuite struct {
    23  	SSHCommonSuite
    24  }
    25  
    26  var _ = gc.Suite(&SSHSuite{})
    27  
    28  var sshTests = []struct {
    29  	about       string
    30  	args        []string
    31  	hostChecker jujussh.ReachableChecker
    32  	isTerminal  bool
    33  	forceAPIv1  bool
    34  	expected    argsSpec
    35  	expectedErr string
    36  }{
    37  	{
    38  		about:       "connect to machine 0 (api v1)",
    39  		args:        []string{"0"},
    40  		hostChecker: validAddresses("0.private", "0.public"),
    41  		forceAPIv1:  true,
    42  		expected: argsSpec{
    43  			hostKeyChecking: "yes",
    44  			knownHosts:      "0",
    45  			args:            "ubuntu@0.public",
    46  		},
    47  	},
    48  	{
    49  		about:       "connect to machine 0 (api v2)",
    50  		args:        []string{"0"},
    51  		hostChecker: validAddresses("0.private", "0.public", "0.1.2.3"), // set by setAddresses() and setLinkLayerDevicesAddresses()
    52  		forceAPIv1:  false,
    53  		expected: argsSpec{
    54  			hostKeyChecking: "yes",
    55  			knownHosts:      "0",
    56  			argsMatch:       `ubuntu@0.(public|private|1\.2\.3)`, // can be any of the 3
    57  		},
    58  	},
    59  	{
    60  		about:       "connect to machine 0 and pass extra arguments",
    61  		args:        []string{"0", "uname", "-a"},
    62  		hostChecker: validAddresses("0.public"),
    63  		expected: argsSpec{
    64  			hostKeyChecking: "yes",
    65  			knownHosts:      "0",
    66  			args:            "ubuntu@0.public uname -a",
    67  		},
    68  	},
    69  	{
    70  		about:       "connect to machine 0 with implied pseudo-tty",
    71  		args:        []string{"0"},
    72  		hostChecker: validAddresses("0.public"),
    73  		isTerminal:  true,
    74  		expected: argsSpec{
    75  			hostKeyChecking: "yes",
    76  			knownHosts:      "0",
    77  			enablePty:       true, // implied by client's terminal
    78  			args:            "ubuntu@0.public",
    79  		},
    80  	},
    81  	{
    82  		about:       "connect to machine 0 with pseudo-tty",
    83  		args:        []string{"--pty=true", "0"},
    84  		hostChecker: validAddresses("0.public"),
    85  		expected: argsSpec{
    86  			hostKeyChecking: "yes",
    87  			knownHosts:      "0",
    88  			enablePty:       true,
    89  			args:            "ubuntu@0.public",
    90  		},
    91  	},
    92  	{
    93  		about:       "connect to machine 0 without pseudo-tty",
    94  		args:        []string{"--pty=false", "0"},
    95  		hostChecker: validAddresses("0.public"),
    96  		isTerminal:  true,
    97  		expected: argsSpec{
    98  			hostKeyChecking: "yes",
    99  			knownHosts:      "0",
   100  			enablePty:       false, // explicitly disabled
   101  			args:            "ubuntu@0.public",
   102  		},
   103  	},
   104  	{
   105  		about:       "connect to machine 1 which has no SSH host keys",
   106  		args:        []string{"1"},
   107  		hostChecker: validAddresses("1.public"),
   108  		expectedErr: `retrieving SSH host keys for "1": keys not found`,
   109  	},
   110  	{
   111  		about:       "connect to machine 1 which has no SSH host keys, no host key checks",
   112  		args:        []string{"--no-host-key-checks", "1"},
   113  		hostChecker: validAddresses("1.public"),
   114  		expected: argsSpec{
   115  			hostKeyChecking: "no",
   116  			knownHosts:      "null",
   117  			args:            "ubuntu@1.public",
   118  		},
   119  	},
   120  	{
   121  		about:       "connect to arbitrary (non-entity) hostname",
   122  		args:        []string{"foo@some.host"},
   123  		hostChecker: validAddresses("some.host"),
   124  		expected: argsSpec{
   125  			// In this case, use the user's own known_hosts and own
   126  			// StrictHostKeyChecking config.
   127  			hostKeyChecking: "",
   128  			knownHosts:      "",
   129  			args:            "foo@some.host",
   130  		},
   131  	},
   132  	{
   133  		about:       "connect to unit mysql/0",
   134  		args:        []string{"mysql/0"},
   135  		hostChecker: validAddresses("0.public"),
   136  		expected: argsSpec{
   137  			hostKeyChecking: "yes",
   138  			knownHosts:      "0",
   139  			args:            "ubuntu@0.public",
   140  		},
   141  	},
   142  	{
   143  		about:       "connect to unit mysql/0 as the mongo user",
   144  		args:        []string{"mongo@mysql/0"},
   145  		hostChecker: validAddresses("0.public"),
   146  		expected: argsSpec{
   147  			hostKeyChecking: "yes",
   148  			knownHosts:      "0",
   149  			args:            "mongo@0.public",
   150  		},
   151  	},
   152  	{
   153  		about:       "connect to unit mysql/0 and pass extra arguments",
   154  		args:        []string{"mysql/0", "ls", "/"},
   155  		hostChecker: validAddresses("0.public"),
   156  		expected: argsSpec{
   157  			hostKeyChecking: "yes",
   158  			knownHosts:      "0",
   159  			args:            "ubuntu@0.public ls /",
   160  		},
   161  	},
   162  	{
   163  		about:       "connect to unit mysql/0 with proxy (api v1)",
   164  		args:        []string{"--proxy=true", "mysql/0"},
   165  		hostChecker: nil, // Host checker shouldn't get used with --proxy=true
   166  		forceAPIv1:  true,
   167  		expected: argsSpec{
   168  			hostKeyChecking: "yes",
   169  			knownHosts:      "0",
   170  			withProxy:       true,
   171  			args:            "ubuntu@0.private",
   172  		},
   173  	},
   174  	{
   175  		about:       "connect to unit mysql/0 with proxy (api v2)",
   176  		args:        []string{"--proxy=true", "mysql/0"},
   177  		hostChecker: nil, // Host checker shouldn't get used with --proxy=true
   178  		forceAPIv1:  false,
   179  		expected: argsSpec{
   180  			hostKeyChecking: "yes",
   181  			knownHosts:      "0",
   182  			withProxy:       true,
   183  			argsMatch:       `ubuntu@0.private`,
   184  		},
   185  	},
   186  }
   187  
   188  func (s *SSHSuite) TestSSHCommand(c *gc.C) {
   189  	s.setupModel(c)
   190  
   191  	for i, t := range sshTests {
   192  		c.Logf("test %d: %s -> %s", i, t.about, t.args)
   193  
   194  		s.setForceAPIv1(t.forceAPIv1)
   195  
   196  		isTerminal := func(stdin interface{}) bool {
   197  			return t.isTerminal
   198  		}
   199  		cmd := newSSHCommand(t.hostChecker, isTerminal)
   200  
   201  		ctx, err := cmdtesting.RunCommand(c, cmd, t.args...)
   202  		if t.expectedErr != "" {
   203  			c.Check(err, gc.ErrorMatches, t.expectedErr)
   204  		} else {
   205  			c.Check(err, jc.ErrorIsNil)
   206  			c.Check(cmdtesting.Stderr(ctx), gc.Equals, "")
   207  			stdout := cmdtesting.Stdout(ctx)
   208  			t.expected.check(c, stdout)
   209  		}
   210  	}
   211  }
   212  
   213  func (s *SSHSuite) TestSSHCommandModelConfigProxySSH(c *gc.C) {
   214  	s.setupModel(c)
   215  
   216  	// Setting proxy-ssh=true in the environment overrides --proxy.
   217  	err := s.Model.UpdateModelConfig(map[string]interface{}{"proxy-ssh": true}, nil)
   218  	c.Assert(err, jc.ErrorIsNil)
   219  
   220  	s.setForceAPIv1(true)
   221  
   222  	ctx, err := cmdtesting.RunCommand(c, newSSHCommand(s.hostChecker, nil), "0")
   223  	c.Check(err, jc.ErrorIsNil)
   224  	c.Check(cmdtesting.Stderr(ctx), gc.Equals, "")
   225  	expectedArgs := argsSpec{
   226  		hostKeyChecking: "yes",
   227  		knownHosts:      "0",
   228  		withProxy:       true,
   229  		args:            "ubuntu@0.private", // as set by setAddresses()
   230  	}
   231  	expectedArgs.check(c, cmdtesting.Stdout(ctx))
   232  
   233  	s.setForceAPIv1(false)
   234  	ctx, err = cmdtesting.RunCommand(c, newSSHCommand(s.hostChecker, nil), "0")
   235  	c.Check(err, jc.ErrorIsNil)
   236  	c.Check(cmdtesting.Stderr(ctx), gc.Equals, "")
   237  	expectedArgs.argsMatch = `ubuntu@0.(public|private|1\.2\.3)` // can be any of the 3 with api v2.
   238  	expectedArgs.check(c, cmdtesting.Stdout(ctx))
   239  
   240  }
   241  
   242  func (s *SSHSuite) TestSSHWillWorkInUpgrade(c *gc.C) {
   243  	// Check the API client interface used by "juju ssh" against what
   244  	// the API server will allow during upgrades. Ensure that the API
   245  	// server will allow all required API calls to support SSH.
   246  	type concrete struct {
   247  		sshAPIClient
   248  	}
   249  	t := reflect.TypeOf(concrete{})
   250  	for i := 0; i < t.NumMethod(); i++ {
   251  		name := t.Method(i).Name
   252  
   253  		// Close isn't an API method.
   254  		if name == "Close" {
   255  			continue
   256  		}
   257  		c.Logf("checking %q", name)
   258  		c.Check(apiserver.IsMethodAllowedDuringUpgrade("SSHClient", name), jc.IsTrue)
   259  	}
   260  }
   261  
   262  /// XXX(jam): 2017-01-25 do we need these functions anymore? We don't really
   263  //support ssh'ing to V1 anymore
   264  func (s *SSHSuite) TestSSHCommandHostAddressRetryAPIv1(c *gc.C) {
   265  	// Start with nothing valid to connect to.
   266  	s.setHostChecker(validAddresses())
   267  	s.setForceAPIv1(true)
   268  
   269  	s.testSSHCommandHostAddressRetry(c, false)
   270  }
   271  
   272  func (s *SSHSuite) TestSSHCommandHostAddressRetryAPIv2(c *gc.C) {
   273  	s.setHostChecker(validAddresses())
   274  	s.setForceAPIv1(false)
   275  
   276  	s.testSSHCommandHostAddressRetry(c, false)
   277  }
   278  
   279  func (s *SSHSuite) TestSSHCommandHostAddressRetryProxyAPIv1(c *gc.C) {
   280  	s.setHostChecker(validAddresses())
   281  	s.setForceAPIv1(true)
   282  
   283  	s.testSSHCommandHostAddressRetry(c, true)
   284  }
   285  
   286  func (s *SSHSuite) TestSSHCommandHostAddressRetryProxyAPIv2(c *gc.C) {
   287  	s.setHostChecker(validAddresses())
   288  	s.setForceAPIv1(false)
   289  
   290  	s.testSSHCommandHostAddressRetry(c, true)
   291  }
   292  
   293  func (s *SSHSuite) testSSHCommandHostAddressRetry(c *gc.C, proxy bool) {
   294  	m := s.Factory.MakeMachine(c, nil)
   295  	s.setKeys(c, m)
   296  
   297  	called := 0
   298  	attemptStarter := &callbackAttemptStarter{next: func() bool {
   299  		called++
   300  		return called < 2
   301  	}}
   302  	restorer := testing.PatchValue(&sshHostFromTargetAttemptStrategy, attemptStarter)
   303  	defer restorer.Restore()
   304  
   305  	// Ensure that the ssh command waits for a public (private with proxy=true)
   306  	// address, or the attempt strategy's Done method returns false.
   307  	args := []string{"--proxy=" + fmt.Sprint(proxy), "0"}
   308  	_, err := cmdtesting.RunCommand(c, newSSHCommand(s.hostChecker, nil), args...)
   309  	c.Assert(err, gc.ErrorMatches, `no .+ address\(es\)`)
   310  	c.Assert(called, gc.Equals, 2)
   311  
   312  	if proxy {
   313  		s.setHostChecker(nil) // not used when proxy=true
   314  	} else {
   315  		s.setHostChecker(validAddresses("0.private", "0.public"))
   316  	}
   317  
   318  	called = 0
   319  	attemptStarter.next = func() bool {
   320  		called++
   321  		if called > 1 {
   322  			s.setAddresses(c, m)
   323  		}
   324  		return true
   325  	}
   326  
   327  	_, err = cmdtesting.RunCommand(c, newSSHCommand(s.hostChecker, nil), args...)
   328  	c.Assert(err, jc.ErrorIsNil)
   329  	c.Assert(called, gc.Equals, 2)
   330  }
   331  
   332  type callbackAttemptStarter struct {
   333  	next func() bool
   334  }
   335  
   336  func (s *callbackAttemptStarter) Start() attempt {
   337  	return callbackAttempt{next: s.next}
   338  }
   339  
   340  type callbackAttempt struct {
   341  	next func() bool
   342  }
   343  
   344  func (a callbackAttempt) Next() bool {
   345  	return a.next()
   346  }