github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/cmd/juju/commands/ssh_test.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"reflect"
    12  	"strings"
    13  
    14  	"github.com/juju/cmd"
    15  	jc "github.com/juju/testing/checkers"
    16  	"github.com/juju/utils/ssh"
    17  	gc "gopkg.in/check.v1"
    18  	"gopkg.in/juju/charm.v6-unstable"
    19  
    20  	"github.com/juju/juju/apiserver"
    21  	"github.com/juju/juju/juju/testing"
    22  	"github.com/juju/juju/network"
    23  	"github.com/juju/juju/state"
    24  	"github.com/juju/juju/testcharms"
    25  	coretesting "github.com/juju/juju/testing"
    26  )
    27  
    28  var _ = gc.Suite(&SSHSuite{})
    29  
    30  type SSHSuite struct {
    31  	SSHCommonSuite
    32  }
    33  
    34  type SSHCommonSuite struct {
    35  	testing.JujuConnSuite
    36  	bin string
    37  }
    38  
    39  func (s *SSHCommonSuite) SetUpTest(c *gc.C) {
    40  	s.JujuConnSuite.SetUpTest(c)
    41  	ssh.ClearClientKeys()
    42  	s.PatchValue(&getJujuExecutable, func() (string, error) { return "juju", nil })
    43  
    44  	s.bin = c.MkDir()
    45  	s.PatchEnvPathPrepend(s.bin)
    46  	for _, name := range patchedCommands {
    47  		f, err := os.OpenFile(filepath.Join(s.bin, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
    48  		c.Assert(err, jc.ErrorIsNil)
    49  		_, err = f.Write([]byte(fakecommand))
    50  		c.Assert(err, jc.ErrorIsNil)
    51  		err = f.Close()
    52  		c.Assert(err, jc.ErrorIsNil)
    53  	}
    54  	client, _ := ssh.NewOpenSSHClient()
    55  	s.PatchValue(&ssh.DefaultClient, client)
    56  }
    57  
    58  const (
    59  	args                = `-o StrictHostKeyChecking no -o PasswordAuthentication no -o ServerAliveInterval 30 `
    60  	withProxy           = `-o StrictHostKeyChecking no -o ProxyCommand juju ssh --proxy=false --pty=false localhost nc %h %p -o PasswordAuthentication no -o ServerAliveInterval 30 `
    61  	commonArgsWithProxy = withProxy + `-o UserKnownHostsFile /dev/null `
    62  	commonArgs          = args + `-o UserKnownHostsFile /dev/null `
    63  	sshArgs             = args + `-t -t -o UserKnownHostsFile /dev/null `
    64  	sshArgsWithProxy    = withProxy + `-t -t -o UserKnownHostsFile /dev/null `
    65  )
    66  
    67  var sshTests = []struct {
    68  	about  string
    69  	args   []string
    70  	result string
    71  }{
    72  	{
    73  		"connect to machine 0",
    74  		[]string{"ssh", "0"},
    75  		sshArgs + "ubuntu@admin-0.dns",
    76  	},
    77  	{
    78  		"connect to machine 0 and pass extra arguments",
    79  		[]string{"ssh", "0", "uname", "-a"},
    80  		sshArgs + "ubuntu@admin-0.dns uname -a",
    81  	},
    82  	{
    83  		"connect to unit mysql/0",
    84  		[]string{"ssh", "mysql/0"},
    85  		sshArgs + "ubuntu@admin-0.dns",
    86  	},
    87  	{
    88  		"connect to unit mongodb/1 as the mongo user",
    89  		[]string{"ssh", "mongo@mongodb/1"},
    90  		sshArgs + "mongo@admin-2.dns",
    91  	},
    92  	{
    93  		"connect to unit mongodb/1 and pass extra arguments",
    94  		[]string{"ssh", "mongodb/1", "ls", "/"},
    95  		sshArgs + "ubuntu@admin-2.dns ls /",
    96  	},
    97  	{
    98  		"connect to unit mysql/0 with proxy",
    99  		[]string{"ssh", "--proxy=true", "mysql/0"},
   100  		sshArgsWithProxy + "ubuntu@admin-0.internal",
   101  	},
   102  }
   103  
   104  func (s *SSHSuite) TestSSHCommand(c *gc.C) {
   105  	m := s.makeMachines(3, c, true)
   106  	ch := testcharms.Repo.CharmDir("dummy")
   107  	curl := charm.MustParseURL(
   108  		fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
   109  	)
   110  	info := state.CharmInfo{
   111  		Charm:       ch,
   112  		ID:          curl,
   113  		StoragePath: "dummy-path",
   114  		SHA256:      "dummy-1-sha256",
   115  	}
   116  	dummy, err := s.State.AddCharm(info)
   117  	c.Assert(err, jc.ErrorIsNil)
   118  	srv := s.AddTestingService(c, "mysql", dummy)
   119  	s.addUnit(srv, m[0], c)
   120  
   121  	srv = s.AddTestingService(c, "mongodb", dummy)
   122  	s.addUnit(srv, m[1], c)
   123  	s.addUnit(srv, m[2], c)
   124  
   125  	for i, t := range sshTests {
   126  		c.Logf("test %d: %s -> %s", i, t.about, t.args)
   127  		ctx := coretesting.Context(c)
   128  		jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{})
   129  		jujucmd.Register(newSSHCommand())
   130  
   131  		code := cmd.Main(jujucmd, ctx, t.args)
   132  		c.Check(code, gc.Equals, 0)
   133  		c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "")
   134  		c.Check(strings.TrimRight(ctx.Stdout.(*bytes.Buffer).String(), "\r\n"), gc.Equals, t.result)
   135  	}
   136  }
   137  
   138  func (s *SSHSuite) TestSSHCommandEnvironProxySSH(c *gc.C) {
   139  	s.makeMachines(1, c, true)
   140  	// Setting proxy-ssh=true in the environment overrides --proxy.
   141  	err := s.State.UpdateModelConfig(map[string]interface{}{"proxy-ssh": true}, nil, nil)
   142  	c.Assert(err, jc.ErrorIsNil)
   143  	ctx := coretesting.Context(c)
   144  	jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{})
   145  	jujucmd.Register(newSSHCommand())
   146  	code := cmd.Main(jujucmd, ctx, []string{"ssh", "0"})
   147  	c.Check(code, gc.Equals, 0)
   148  	c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "")
   149  	c.Check(strings.TrimRight(ctx.Stdout.(*bytes.Buffer).String(), "\r\n"), gc.Equals, sshArgsWithProxy+"ubuntu@admin-0.internal")
   150  }
   151  
   152  func (s *SSHSuite) TestSSHWillWorkInUpgrade(c *gc.C) {
   153  	// Check the API client interface used by "juju ssh" against what
   154  	// the API server will allow during upgrades. Ensure that the API
   155  	// server will allow all required API calls to support SSH.
   156  	type concrete struct {
   157  		sshAPIClient
   158  	}
   159  	t := reflect.TypeOf(concrete{})
   160  	for i := 0; i < t.NumMethod(); i++ {
   161  		name := t.Method(i).Name
   162  
   163  		// Close isn't an API method.
   164  		if name == "Close" {
   165  			continue
   166  		}
   167  		c.Logf("checking %q", name)
   168  		c.Check(apiserver.IsMethodAllowedDuringUpgrade("Client", name), jc.IsTrue)
   169  	}
   170  }
   171  
   172  type callbackAttemptStarter struct {
   173  	next func() bool
   174  }
   175  
   176  func (s *callbackAttemptStarter) Start() attempt {
   177  	return callbackAttempt{next: s.next}
   178  }
   179  
   180  type callbackAttempt struct {
   181  	next func() bool
   182  }
   183  
   184  func (a callbackAttempt) Next() bool {
   185  	return a.next()
   186  }
   187  
   188  func (s *SSHSuite) TestSSHCommandHostAddressRetry(c *gc.C) {
   189  	s.testSSHCommandHostAddressRetry(c, false)
   190  }
   191  
   192  func (s *SSHSuite) TestSSHCommandHostAddressRetryProxy(c *gc.C) {
   193  	s.testSSHCommandHostAddressRetry(c, true)
   194  }
   195  
   196  func (s *SSHSuite) testSSHCommandHostAddressRetry(c *gc.C, proxy bool) {
   197  	m := s.makeMachines(1, c, false)
   198  	ctx := coretesting.Context(c)
   199  
   200  	var called int
   201  	next := func() bool {
   202  		called++
   203  		return called < 2
   204  	}
   205  	attemptStarter := &callbackAttemptStarter{next: next}
   206  	s.PatchValue(&sshHostFromTargetAttemptStrategy, attemptStarter)
   207  
   208  	// Ensure that the ssh command waits for a public address, or the attempt
   209  	// strategy's Done method returns false.
   210  	args := []string{"--proxy=" + fmt.Sprint(proxy), "0"}
   211  	code := cmd.Main(newSSHCommand(), ctx, args)
   212  	c.Check(code, gc.Equals, 1)
   213  	c.Assert(called, gc.Equals, 2)
   214  	called = 0
   215  	attemptStarter.next = func() bool {
   216  		called++
   217  		if called > 1 {
   218  			s.setAddresses(m[0], c)
   219  		}
   220  		return true
   221  	}
   222  	code = cmd.Main(newSSHCommand(), ctx, args)
   223  	c.Check(code, gc.Equals, 0)
   224  	c.Assert(called, gc.Equals, 2)
   225  }
   226  
   227  func (s *SSHCommonSuite) setAddresses(m *state.Machine, c *gc.C) {
   228  	addrPub := network.NewScopedAddress(
   229  		fmt.Sprintf("admin-%s.dns", m.Id()),
   230  		network.ScopePublic,
   231  	)
   232  	addrPriv := network.NewScopedAddress(
   233  		fmt.Sprintf("admin-%s.internal", m.Id()),
   234  		network.ScopeCloudLocal,
   235  	)
   236  	err := m.SetProviderAddresses(addrPub, addrPriv)
   237  	c.Assert(err, jc.ErrorIsNil)
   238  }
   239  
   240  func (s *SSHCommonSuite) makeMachines(n int, c *gc.C, setAddresses bool) []*state.Machine {
   241  	var machines = make([]*state.Machine, n)
   242  	for i := 0; i < n; i++ {
   243  		m, err := s.State.AddMachine("quantal", state.JobHostUnits)
   244  		c.Assert(err, jc.ErrorIsNil)
   245  		if setAddresses {
   246  			s.setAddresses(m, c)
   247  		}
   248  		// must set an instance id as the ssh command uses that as a signal the
   249  		// machine has been provisioned
   250  		inst, md := testing.AssertStartInstance(c, s.Environ, m.Id())
   251  		c.Assert(m.SetProvisioned(inst.Id(), "fake_nonce", md), gc.IsNil)
   252  		machines[i] = m
   253  	}
   254  	return machines
   255  }
   256  
   257  func (s *SSHCommonSuite) addUnit(srv *state.Service, m *state.Machine, c *gc.C) {
   258  	u, err := srv.AddUnit()
   259  	c.Assert(err, jc.ErrorIsNil)
   260  	err = u.AssignToMachine(m)
   261  	c.Assert(err, jc.ErrorIsNil)
   262  }