github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/commands/ssh_common_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package commands 5 6 import ( 7 "fmt" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strings" 12 13 "github.com/juju/collections/set" 14 "github.com/juju/errors" 15 jc "github.com/juju/testing/checkers" 16 "github.com/juju/utils/ssh" 17 gc "gopkg.in/check.v1" 18 19 "github.com/juju/juju/juju/testing" 20 "github.com/juju/juju/network" 21 jujussh "github.com/juju/juju/network/ssh" 22 "github.com/juju/juju/state" 23 ) 24 25 // argsSpec is a test helper which converts a number of options into 26 // expected ssh/scp command lines. 27 type argsSpec struct { 28 // hostKeyChecking specifies the expected StrictHostKeyChecking 29 // option. 30 hostKeyChecking string 31 32 // withProxy specifies if the juju ProxyCommand option is 33 // expected. 34 withProxy bool 35 36 // enablePty specifies if the forced PTY allocation switches are 37 // expected. 38 enablePty bool 39 40 // knownHosts may either be: 41 // a comma separated list of machine ids - the host keys for these 42 // machines are expected in the UserKnownHostsFile 43 // "null" - the UserKnownHostsFile must be "/dev/null" 44 // empty - no UserKnownHostsFile option expected 45 knownHosts string 46 47 // args specifies any other command line arguments expected. This 48 // includes the SSH/SCP targets. Ignored if argsMatch is set as well. 49 args string 50 51 // argsMatch is like args, but instead of a literal string it's interpreted 52 // as a regular expression. When argsMatch is set, args is ignored. 53 argsMatch string 54 } 55 56 func (s *argsSpec) check(c *gc.C, output string) { 57 // The first line in the output from the fake ssh/scp is the 58 // command line. The remaining lines should contain the contents 59 // of the UserKnownHostsFile file provided (if any). 60 parts := strings.SplitN(output, "\n", 2) 61 actualCommandLine := parts[0] 62 actualKnownHosts := "" 63 if len(parts) == 2 { 64 actualKnownHosts = parts[1] 65 } 66 67 var expected []string 68 expect := func(part string) { 69 expected = append(expected, part) 70 } 71 if s.hostKeyChecking != "" { 72 expect("-o StrictHostKeyChecking " + s.hostKeyChecking) 73 } 74 75 if s.withProxy { 76 expect("-o ProxyCommand juju ssh " + 77 "--model=controller " + 78 "--proxy=false " + 79 "--no-host-key-checks " + 80 "--pty=false ubuntu@localhost -q \"nc %h %p\"") 81 } 82 expect("-o PasswordAuthentication no -o ServerAliveInterval 30") 83 if s.enablePty { 84 expect("-t -t") 85 } 86 if s.knownHosts == "null" { 87 expect(`-o UserKnownHostsFile /dev/null`) 88 } else if s.knownHosts == "" { 89 // No UserKnownHostsFile option expected. 90 } else { 91 expect(`-o UserKnownHostsFile \S+`) 92 93 // Check that the provided known_hosts file contained the 94 // expected keys. 95 c.Check(actualKnownHosts, gc.Matches, s.expectedKnownHosts()) 96 } 97 98 if s.argsMatch != "" { 99 expect(s.argsMatch) 100 } else { 101 expect(regexp.QuoteMeta(s.args)) 102 } 103 104 // Check the command line matches what is expected. 105 pattern := "^" + strings.Join(expected, " ") + "$" 106 c.Check(actualCommandLine, gc.Matches, pattern) 107 } 108 109 func (s *argsSpec) expectedKnownHosts() string { 110 out := "" 111 for _, id := range strings.Split(s.knownHosts, ",") { 112 out += fmt.Sprintf(".+ dsa-%s\n.+ rsa-%s\n", id, id) 113 } 114 return out 115 } 116 117 type SSHCommonSuite struct { 118 testing.JujuConnSuite 119 knownHostsDir string 120 binDir string 121 hostChecker jujussh.ReachableChecker 122 } 123 124 // Commands to patch 125 var patchedCommands = []string{"ssh", "scp"} 126 127 // fakecommand outputs its arguments to stdout for verification 128 var fakecommand = `#!/bin/bash 129 130 { 131 echo "$@" 132 133 # If a custom known_hosts file was passed, emit the contents of 134 # that too. 135 while (( "$#" )); do 136 if [[ $1 = UserKnownHostsFile* ]]; then 137 IFS=" " read -ra parts <<< $1 138 cat "${parts[1]}" 139 break 140 fi 141 shift 142 done 143 }| tee $0.args 144 ` 145 146 type fakeHostChecker struct { 147 acceptedAddresses set.Strings 148 } 149 150 var _ jujussh.ReachableChecker = (*fakeHostChecker)(nil) 151 152 func (f *fakeHostChecker) FindHost(hostPorts []network.HostPort, publicKeys []string) (network.HostPort, error) { 153 // TODO(jam): The real reachable checker won't give deterministic ordering 154 // for hostPorts, maybe we should do a random return value? 155 for _, hostPort := range hostPorts { 156 if f.acceptedAddresses.Contains(hostPort.Address.Value) { 157 return hostPort, nil 158 } 159 } 160 return network.HostPort{}, errors.Errorf("cannot connect to any address: %v", hostPorts) 161 } 162 163 func validAddresses(acceptedAddresses ...string) *fakeHostChecker { 164 return &fakeHostChecker{ 165 acceptedAddresses: set.NewStrings(acceptedAddresses...), 166 } 167 } 168 169 func (s *SSHCommonSuite) SetUpTest(c *gc.C) { 170 s.JujuConnSuite.SetUpTest(c) 171 ssh.ClearClientKeys() 172 s.PatchValue(&getJujuExecutable, func() (string, error) { return "juju", nil }) 173 s.setForceAPIv1(false) 174 175 s.binDir = c.MkDir() 176 s.PatchEnvPathPrepend(s.binDir) 177 for _, name := range patchedCommands { 178 f, err := os.OpenFile(filepath.Join(s.binDir, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777) 179 c.Assert(err, jc.ErrorIsNil) 180 _, err = f.Write([]byte(fakecommand)) 181 c.Assert(err, jc.ErrorIsNil) 182 err = f.Close() 183 c.Assert(err, jc.ErrorIsNil) 184 } 185 186 client, _ := ssh.NewOpenSSHClient() 187 s.PatchValue(&ssh.DefaultClient, client) 188 } 189 190 func (s *SSHCommonSuite) setForceAPIv1(enabled bool) { 191 if enabled { 192 os.Setenv(jujuSSHClientForceAPIv1, "1") 193 } else { 194 os.Unsetenv(jujuSSHClientForceAPIv1) 195 } 196 } 197 198 func (s *SSHCommonSuite) setHostChecker(hostChecker jujussh.ReachableChecker) { 199 s.hostChecker = hostChecker 200 } 201 202 func (s *SSHCommonSuite) setupModel(c *gc.C) { 203 // Add machine-0 with a mysql application and mysql/0 unit 204 u := s.Factory.MakeUnit(c, nil) 205 206 // Set both the preferred public and private addresses for machine-0, add a 207 // couple of link-layer devices (loopback and ethernet) with addresses, and 208 // the ssh keys. 209 m := s.getMachineForUnit(c, u) 210 s.setAddresses(c, m) 211 s.setKeys(c, m) 212 s.setLinkLayerDevicesAddresses(c, m) 213 214 // machine-1 has no public host keys available. 215 m1 := s.Factory.MakeMachine(c, nil) 216 s.setAddresses(c, m1) 217 218 // machine-2 has IPv6 addresses 219 m2 := s.Factory.MakeMachine(c, nil) 220 s.setAddresses6(c, m2) 221 s.setKeys(c, m2) 222 } 223 224 func (s *SSHCommonSuite) getMachineForUnit(c *gc.C, u *state.Unit) *state.Machine { 225 machineId, err := u.AssignedMachineId() 226 c.Assert(err, jc.ErrorIsNil) 227 m, err := s.State.Machine(machineId) 228 c.Assert(err, jc.ErrorIsNil) 229 return m 230 } 231 232 func (s *SSHCommonSuite) setAddresses(c *gc.C, m *state.Machine) { 233 addrPub := network.NewScopedAddress( 234 fmt.Sprintf("%s.public", m.Id()), 235 network.ScopePublic, 236 ) 237 addrPriv := network.NewScopedAddress( 238 fmt.Sprintf("%s.private", m.Id()), 239 network.ScopeCloudLocal, 240 ) 241 err := m.SetProviderAddresses(addrPub, addrPriv) 242 c.Assert(err, jc.ErrorIsNil) 243 } 244 245 func (s *SSHCommonSuite) setLinkLayerDevicesAddresses(c *gc.C, m *state.Machine) { 246 devicesArgs := []state.LinkLayerDeviceArgs{{ 247 Name: "lo", 248 Type: state.LoopbackDevice, 249 }, { 250 Name: "eth0", 251 Type: state.EthernetDevice, 252 }} 253 err := m.SetLinkLayerDevices(devicesArgs...) 254 c.Assert(err, jc.ErrorIsNil) 255 256 addressesArgs := []state.LinkLayerDeviceAddress{{ 257 DeviceName: "lo", 258 CIDRAddress: "127.0.0.1/8", // will be filtered 259 ConfigMethod: state.LoopbackAddress, 260 }, { 261 DeviceName: "eth0", 262 CIDRAddress: "0.1.2.3/24", // needs to be a valid CIDR 263 ConfigMethod: state.StaticAddress, 264 }} 265 err = m.SetDevicesAddresses(addressesArgs...) 266 c.Assert(err, jc.ErrorIsNil) 267 } 268 269 func (s *SSHCommonSuite) setAddresses6(c *gc.C, m *state.Machine) { 270 addrPub := network.NewScopedAddress("2001:db8::1", network.ScopePublic) 271 addrPriv := network.NewScopedAddress("fc00:bbb::1", network.ScopeCloudLocal) 272 err := m.SetProviderAddresses(addrPub, addrPriv) 273 c.Assert(err, jc.ErrorIsNil) 274 } 275 276 func (s *SSHCommonSuite) setKeys(c *gc.C, m *state.Machine) { 277 id := m.Id() 278 keys := state.SSHHostKeys{"dsa-" + id, "rsa-" + id} 279 err := s.State.SetSSHHostKeys(m.MachineTag(), keys) 280 c.Assert(err, jc.ErrorIsNil) 281 }