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 }