github.phpd.cn/cilium/cilium@v1.6.12/test/helpers/ssh_command.go (about) 1 // Copyright 2017-2019 Authors of Cilium 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package helpers 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "net" 24 "os" 25 "strconv" 26 "time" 27 28 "github.com/kevinburke/ssh_config" 29 "golang.org/x/crypto/ssh" 30 "golang.org/x/crypto/ssh/agent" 31 ) 32 33 // SSHCommand stores the data associated with executing a command. 34 // TODO: this is poorly named in that it's not related to a command only 35 // ran over SSH - rename this. 36 type SSHCommand struct { 37 // TODO: path is not a clear name - rename to something more clear. 38 Path string 39 Env []string 40 Stdin io.Reader 41 Stdout io.Writer 42 Stderr io.Writer 43 } 44 45 // SSHClient stores the information needed to SSH into a remote location for 46 // running tests. 47 type SSHClient struct { 48 Config *ssh.ClientConfig // ssh client configuration information. 49 Host string // Ip/Host from the target virtualserver 50 Port int // Port to connect to the target server 51 client *ssh.Client // Client implements a traditional SSH client that supports shells, 52 // subprocesses, TCP port/streamlocal forwarding and tunneled dialing. 53 } 54 55 // GetHostPort returns the host port representation of the ssh client 56 func (cli *SSHClient) GetHostPort() string { 57 return net.JoinHostPort(cli.Host, strconv.Itoa(cli.Port)) 58 } 59 60 // SSHConfig contains metadata for an SSH session. 61 type SSHConfig struct { 62 target string 63 host string 64 user string 65 port int 66 identityFile string 67 } 68 69 // SSHConfigs maps the name of a host (VM) to its corresponding SSHConfiguration 70 type SSHConfigs map[string]*SSHConfig 71 72 // GetSSHClient initializes an SSHClient based on the provided SSHConfig 73 func (cfg *SSHConfig) GetSSHClient() *SSHClient { 74 sshConfig := &ssh.ClientConfig{ 75 User: cfg.user, 76 Auth: []ssh.AuthMethod{ 77 cfg.GetSSHAgent(), 78 }, 79 HostKeyCallback: ssh.InsecureIgnoreHostKey(), 80 Timeout: 15 * time.Second, 81 } 82 83 return &SSHClient{ 84 Config: sshConfig, 85 Host: cfg.host, 86 Port: cfg.port, 87 } 88 } 89 90 func (client *SSHClient) String() string { 91 return fmt.Sprintf("host: %s, port: %d, user: %s", client.Host, client.Port, client.Config.User) 92 } 93 94 func (cfg *SSHConfig) String() string { 95 return fmt.Sprintf("target: %s, host: %s, port %d, user, %s, identityFile: %s", cfg.target, cfg.host, cfg.port, cfg.user, cfg.identityFile) 96 } 97 98 // GetSSHAgent returns the ssh.AuthMethod corresponding to SSHConfig cfg. 99 func (cfg *SSHConfig) GetSSHAgent() ssh.AuthMethod { 100 key, err := ioutil.ReadFile(cfg.identityFile) 101 if err != nil { 102 log.Fatalf("unable to retrieve ssh-key on target '%s': %s", cfg.target, err) 103 } 104 105 signer, err := ssh.ParsePrivateKey(key) 106 if err != nil { 107 log.Fatalf("unable to parse private key on target '%s': %s", cfg.target, err) 108 } 109 return ssh.PublicKeys(signer) 110 } 111 112 // ImportSSHconfig imports the SSH configuration stored at the provided path. 113 // Returns an error if the SSH configuration could not be instantiated. 114 func ImportSSHconfig(config []byte) (SSHConfigs, error) { 115 result := make(SSHConfigs) 116 cfg, err := ssh_config.Decode(bytes.NewBuffer(config)) 117 if err != nil { 118 return nil, err 119 } 120 121 for _, host := range cfg.Hosts { 122 key := host.Patterns[0].String() 123 if key == "*" { 124 continue 125 } 126 port, _ := cfg.Get(key, "Port") 127 hostConfig := SSHConfig{target: key} 128 hostConfig.host, _ = cfg.Get(key, "Hostname") 129 hostConfig.identityFile, _ = cfg.Get(key, "identityFile") 130 hostConfig.user, _ = cfg.Get(key, "User") 131 hostConfig.port, _ = strconv.Atoi(port) 132 result[key] = &hostConfig 133 } 134 return result, nil 135 } 136 137 // copyWait runs an instance of io.Copy() in a goroutine, and returns a channel 138 // to receive the error result. 139 func copyWait(dst io.Writer, src io.Reader) chan error { 140 c := make(chan error, 1) 141 go func() { 142 _, err := io.Copy(dst, src) 143 c <- err 144 }() 145 return c 146 } 147 148 // runCommand runs the specified command on the provided SSH session, and 149 // gathers both of the sterr and stdout output into the writers provided by 150 // cmd. Returns whether the command was run and an optional error. 151 // Returns nil when the command completes successfully and all stderr, 152 // stdout output has been written. Returns an error otherwise. 153 func runCommand(session *ssh.Session, cmd *SSHCommand) (bool, error) { 154 stderr, err := session.StderrPipe() 155 if err != nil { 156 return false, fmt.Errorf("Unable to setup stderr for session: %v", err) 157 } 158 errChan := copyWait(cmd.Stderr, stderr) 159 160 stdout, err := session.StdoutPipe() 161 if err != nil { 162 return false, fmt.Errorf("Unable to setup stdout for session: %v", err) 163 } 164 outChan := copyWait(cmd.Stdout, stdout) 165 166 if err = session.Run(cmd.Path); err != nil { 167 return false, err 168 } 169 170 if err = <-errChan; err != nil { 171 return true, err 172 } 173 if err = <-outChan; err != nil { 174 return true, err 175 } 176 return true, nil 177 } 178 179 // RunCommand runs a SSHCommand using SSHClient client. The returned error is 180 // nil if the command runs, has no problems copying stdin, stdout, and stderr, 181 // and exits with a zero exit status. 182 func (client *SSHClient) RunCommand(cmd *SSHCommand) error { 183 session, err := client.newSession() 184 if err != nil { 185 return err 186 } 187 defer session.Close() 188 189 _, err = runCommand(session, cmd) 190 return err 191 } 192 193 // RunCommandInBackground runs an SSH command in a similar way to 194 // RunCommandContext, but with a context which allows the command to be 195 // cancelled at any time. When cancel is called the error of the command is 196 // returned instead the context error. 197 func (client *SSHClient) RunCommandInBackground(ctx context.Context, cmd *SSHCommand) error { 198 if ctx == nil { 199 panic("nil context provided to RunCommandInBackground()") 200 } 201 202 session, err := client.newSession() 203 if err != nil { 204 return err 205 } 206 defer session.Close() 207 208 modes := ssh.TerminalModes{ 209 ssh.ECHO: 1, // enable echoing 210 ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 211 ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 212 } 213 session.RequestPty("xterm-256color", 80, 80, modes) 214 215 stdin, err := session.StdinPipe() 216 if err != nil { 217 log.Errorf("Could not get stdin: %s", err) 218 } 219 220 go func() { 221 select { 222 case <-ctx.Done(): 223 _, err := stdin.Write([]byte{3}) 224 if err != nil { 225 log.Errorf("write ^C error: %s", err) 226 } 227 err = session.Wait() 228 if err != nil { 229 log.Errorf("wait error: %s", err) 230 } 231 if err = session.Signal(ssh.SIGHUP); err != nil { 232 log.Errorf("failed to kill command: %s", err) 233 } 234 if err = session.Close(); err != nil { 235 log.Errorf("failed to close session: %s", err) 236 } 237 } 238 }() 239 _, err = runCommand(session, cmd) 240 return err 241 } 242 243 // RunCommandContext runs an SSH command in a similar way to RunCommand but with 244 // a context. If context is canceled it will return the error of that given 245 // context. 246 func (client *SSHClient) RunCommandContext(ctx context.Context, cmd *SSHCommand) error { 247 if ctx == nil { 248 panic("nil context provided to RunCommandContext()") 249 } 250 251 var ( 252 session *ssh.Session 253 sessionErrChan = make(chan error, 1) 254 ) 255 256 go func() { 257 var sessionErr error 258 259 // This may block depending on the state of the setup tests are being 260 // ran against. As a result, these goroutines may leak, but the logic 261 // below will fail and propagate to the rest of the CI framework, which 262 // will error out anyway. It's better to leak in really bad cases since 263 // the CI will fail anyway. Unfortunately, the golang SSH library does 264 // not provide a way to propagate context through to creating sessions. 265 266 // Note that this is a closure on the session variable! 267 session, sessionErr = client.newSession() 268 if sessionErr != nil { 269 log.Infof("error creating session: %s", sessionErr) 270 sessionErrChan <- sessionErr 271 return 272 } 273 274 _, runErr := runCommand(session, cmd) 275 sessionErrChan <- runErr 276 }() 277 278 select { 279 case asyncErr := <-sessionErrChan: 280 return asyncErr 281 case <-ctx.Done(): 282 if session != nil { 283 log.Warning("sending SIGHUP to session due to canceled context") 284 if err := session.Signal(ssh.SIGHUP); err != nil { 285 log.Errorf("failed to kill command when context is canceled: %s", err) 286 } 287 if closeErr := session.Close(); closeErr != nil { 288 log.WithError(closeErr).Error("failed to close session") 289 } 290 } else { 291 log.Error("timeout reached; no session was able to be created") 292 } 293 return ctx.Err() 294 } 295 } 296 297 func (client *SSHClient) newSession() (*ssh.Session, error) { 298 var connection *ssh.Client 299 var err error 300 301 if client.client != nil { 302 connection = client.client 303 } else { 304 connection, err = ssh.Dial( 305 "tcp", 306 fmt.Sprintf("%s:%d", client.Host, client.Port), 307 client.Config) 308 309 if err != nil { 310 return nil, fmt.Errorf("failed to dial: %s", err) 311 } 312 client.client = connection 313 } 314 315 session, err := connection.NewSession() 316 if err != nil { 317 return nil, fmt.Errorf("failed to create session: %s", err) 318 } 319 320 return session, nil 321 } 322 323 // SSHAgent returns the ssh.Authmethod using the Public keys. Returns nil if 324 // a connection to SSH_AUTH_SHOCK does not succeed. 325 func SSHAgent() ssh.AuthMethod { 326 if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { 327 return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) 328 } 329 return nil 330 } 331 332 // GetSSHClient initializes an SSHClient for the specified host/port/user 333 // combination. 334 func GetSSHClient(host string, port int, user string) *SSHClient { 335 336 sshConfig := &ssh.ClientConfig{ 337 User: user, 338 Auth: []ssh.AuthMethod{ 339 SSHAgent(), 340 }, 341 HostKeyCallback: ssh.InsecureIgnoreHostKey(), 342 Timeout: 15 * time.Second, 343 } 344 345 return &SSHClient{ 346 Config: sshConfig, 347 Host: host, 348 Port: port, 349 } 350 351 }