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 }