github.com/bendemaree/terraform@v0.5.4-0.20150613200311-f50d97d6eee6/communicator/ssh/communicator.go (about) 1 package ssh 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "log" 11 "math/rand" 12 "net" 13 "os" 14 "path/filepath" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/hashicorp/terraform/communicator/remote" 20 "github.com/hashicorp/terraform/terraform" 21 "golang.org/x/crypto/ssh" 22 ) 23 24 const ( 25 // DefaultShebang is added at the top of a SSH script file 26 DefaultShebang = "#!/bin/sh\n" 27 ) 28 29 // Communicator represents the SSH communicator 30 type Communicator struct { 31 connInfo *connectionInfo 32 client *ssh.Client 33 config *sshConfig 34 conn net.Conn 35 address string 36 } 37 38 type sshConfig struct { 39 // The configuration of the Go SSH connection 40 config *ssh.ClientConfig 41 42 // connection returns a new connection. The current connection 43 // in use will be closed as part of the Close method, or in the 44 // case an error occurs. 45 connection func() (net.Conn, error) 46 47 // noPty, if true, will not request a pty from the remote end. 48 noPty bool 49 50 // sshAgentConn is a pointer to the UNIX connection for talking with the 51 // ssh-agent. 52 sshAgentConn net.Conn 53 } 54 55 // New creates a new communicator implementation over SSH. 56 func New(s *terraform.InstanceState) (*Communicator, error) { 57 connInfo, err := parseConnectionInfo(s) 58 if err != nil { 59 return nil, err 60 } 61 62 config, err := prepareSSHConfig(connInfo) 63 if err != nil { 64 return nil, err 65 } 66 67 comm := &Communicator{ 68 connInfo: connInfo, 69 config: config, 70 } 71 72 return comm, nil 73 } 74 75 // Connect implementation of communicator.Communicator interface 76 func (c *Communicator) Connect(o terraform.UIOutput) (err error) { 77 if c.conn != nil { 78 c.conn.Close() 79 } 80 81 // Set the conn and client to nil since we'll recreate it 82 c.conn = nil 83 c.client = nil 84 85 if o != nil { 86 o.Output(fmt.Sprintf( 87 "Connecting to remote host via SSH...\n"+ 88 " Host: %s\n"+ 89 " User: %s\n"+ 90 " Password: %t\n"+ 91 " Private key: %t\n"+ 92 " SSH Agent: %t", 93 c.connInfo.Host, c.connInfo.User, 94 c.connInfo.Password != "", 95 c.connInfo.KeyFile != "", 96 c.connInfo.Agent, 97 )) 98 } 99 100 log.Printf("connecting to TCP connection for SSH") 101 c.conn, err = c.config.connection() 102 if err != nil { 103 // Explicitly set this to the REAL nil. Connection() can return 104 // a nil implementation of net.Conn which will make the 105 // "if c.conn == nil" check fail above. Read here for more information 106 // on this psychotic language feature: 107 // 108 // http://golang.org/doc/faq#nil_error 109 c.conn = nil 110 111 log.Printf("connection error: %s", err) 112 return err 113 } 114 115 log.Printf("handshaking with SSH") 116 host := fmt.Sprintf("%s:%d", c.connInfo.Host, c.connInfo.Port) 117 sshConn, sshChan, req, err := ssh.NewClientConn(c.conn, host, c.config.config) 118 if err != nil { 119 log.Printf("handshake error: %s", err) 120 return err 121 } 122 123 c.client = ssh.NewClient(sshConn, sshChan, req) 124 125 if o != nil { 126 o.Output("Connected!") 127 } 128 129 return err 130 } 131 132 // Disconnect implementation of communicator.Communicator interface 133 func (c *Communicator) Disconnect() error { 134 if c.config.sshAgentConn != nil { 135 return c.config.sshAgentConn.Close() 136 } 137 138 return nil 139 } 140 141 // Timeout implementation of communicator.Communicator interface 142 func (c *Communicator) Timeout() time.Duration { 143 return c.connInfo.TimeoutVal 144 } 145 146 // ScriptPath implementation of communicator.Communicator interface 147 func (c *Communicator) ScriptPath() string { 148 return strings.Replace( 149 c.connInfo.ScriptPath, "%RAND%", 150 strconv.FormatInt(int64(rand.Int31()), 10), -1) 151 } 152 153 // Start implementation of communicator.Communicator interface 154 func (c *Communicator) Start(cmd *remote.Cmd) error { 155 session, err := c.newSession() 156 if err != nil { 157 return err 158 } 159 160 // Setup our session 161 session.Stdin = cmd.Stdin 162 session.Stdout = cmd.Stdout 163 session.Stderr = cmd.Stderr 164 165 if !c.config.noPty { 166 // Request a PTY 167 termModes := ssh.TerminalModes{ 168 ssh.ECHO: 0, // do not echo 169 ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 170 ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 171 } 172 173 if err := session.RequestPty("xterm", 80, 40, termModes); err != nil { 174 return err 175 } 176 } 177 178 log.Printf("starting remote command: %s", cmd.Command) 179 err = session.Start(cmd.Command + "\n") 180 if err != nil { 181 return err 182 } 183 184 // Start a goroutine to wait for the session to end and set the 185 // exit boolean and status. 186 go func() { 187 defer session.Close() 188 189 err := session.Wait() 190 exitStatus := 0 191 if err != nil { 192 exitErr, ok := err.(*ssh.ExitError) 193 if ok { 194 exitStatus = exitErr.ExitStatus() 195 } 196 } 197 198 log.Printf("remote command exited with '%d': %s", exitStatus, cmd.Command) 199 cmd.SetExited(exitStatus) 200 }() 201 202 return nil 203 } 204 205 // Upload implementation of communicator.Communicator interface 206 func (c *Communicator) Upload(path string, input io.Reader) error { 207 // The target directory and file for talking the SCP protocol 208 targetDir := filepath.Dir(path) 209 targetFile := filepath.Base(path) 210 211 // On windows, filepath.Dir uses backslash separators (ie. "\tmp"). 212 // This does not work when the target host is unix. Switch to forward slash 213 // which works for unix and windows 214 targetDir = filepath.ToSlash(targetDir) 215 216 scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error { 217 return scpUploadFile(targetFile, input, w, stdoutR) 218 } 219 220 return c.scpSession("scp -vt "+targetDir, scpFunc) 221 } 222 223 // UploadScript implementation of communicator.Communicator interface 224 func (c *Communicator) UploadScript(path string, input io.Reader) error { 225 reader := bufio.NewReader(input) 226 prefix, err := reader.Peek(2) 227 if err != nil { 228 return fmt.Errorf("Error reading script: %s", err) 229 } 230 231 var script bytes.Buffer 232 if string(prefix) != "#!" { 233 script.WriteString(DefaultShebang) 234 } 235 236 script.ReadFrom(reader) 237 if err := c.Upload(path, &script); err != nil { 238 return err 239 } 240 241 var stdout, stderr bytes.Buffer 242 cmd := &remote.Cmd{ 243 Command: fmt.Sprintf("chmod 0777 %s", path), 244 Stdout: &stdout, 245 Stderr: &stderr, 246 } 247 if err := c.Start(cmd); err != nil { 248 return fmt.Errorf( 249 "Error chmodding script file to 0777 in remote "+ 250 "machine: %s", err) 251 } 252 cmd.Wait() 253 if cmd.ExitStatus != 0 { 254 return fmt.Errorf( 255 "Error chmodding script file to 0777 in remote "+ 256 "machine %d: %s %s", cmd.ExitStatus, stdout.String(), stderr.String()) 257 } 258 259 return nil 260 } 261 262 // UploadDir implementation of communicator.Communicator interface 263 func (c *Communicator) UploadDir(dst string, src string) error { 264 log.Printf("Uploading dir '%s' to '%s'", src, dst) 265 scpFunc := func(w io.Writer, r *bufio.Reader) error { 266 uploadEntries := func() error { 267 f, err := os.Open(src) 268 if err != nil { 269 return err 270 } 271 defer f.Close() 272 273 entries, err := f.Readdir(-1) 274 if err != nil { 275 return err 276 } 277 278 return scpUploadDir(src, entries, w, r) 279 } 280 281 if src[len(src)-1] != '/' { 282 log.Printf("No trailing slash, creating the source directory name") 283 return scpUploadDirProtocol(filepath.Base(src), w, r, uploadEntries) 284 } 285 // Trailing slash, so only upload the contents 286 return uploadEntries() 287 } 288 289 return c.scpSession("scp -rvt "+dst, scpFunc) 290 } 291 292 func (c *Communicator) newSession() (session *ssh.Session, err error) { 293 log.Println("opening new ssh session") 294 if c.client == nil { 295 err = errors.New("client not available") 296 } else { 297 session, err = c.client.NewSession() 298 } 299 300 if err != nil { 301 log.Printf("ssh session open error: '%s', attempting reconnect", err) 302 if err := c.Connect(nil); err != nil { 303 return nil, err 304 } 305 306 return c.client.NewSession() 307 } 308 309 return session, nil 310 } 311 312 func (c *Communicator) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error { 313 session, err := c.newSession() 314 if err != nil { 315 return err 316 } 317 defer session.Close() 318 319 // Get a pipe to stdin so that we can send data down 320 stdinW, err := session.StdinPipe() 321 if err != nil { 322 return err 323 } 324 325 // We only want to close once, so we nil w after we close it, 326 // and only close in the defer if it hasn't been closed already. 327 defer func() { 328 if stdinW != nil { 329 stdinW.Close() 330 } 331 }() 332 333 // Get a pipe to stdout so that we can get responses back 334 stdoutPipe, err := session.StdoutPipe() 335 if err != nil { 336 return err 337 } 338 stdoutR := bufio.NewReader(stdoutPipe) 339 340 // Set stderr to a bytes buffer 341 stderr := new(bytes.Buffer) 342 session.Stderr = stderr 343 344 // Start the sink mode on the other side 345 // TODO(mitchellh): There are probably issues with shell escaping the path 346 log.Println("Starting remote scp process: ", scpCommand) 347 if err := session.Start(scpCommand); err != nil { 348 return err 349 } 350 351 // Call our callback that executes in the context of SCP. We ignore 352 // EOF errors if they occur because it usually means that SCP prematurely 353 // ended on the other side. 354 log.Println("Started SCP session, beginning transfers...") 355 if err := f(stdinW, stdoutR); err != nil && err != io.EOF { 356 return err 357 } 358 359 // Close the stdin, which sends an EOF, and then set w to nil so that 360 // our defer func doesn't close it again since that is unsafe with 361 // the Go SSH package. 362 log.Println("SCP session complete, closing stdin pipe.") 363 stdinW.Close() 364 stdinW = nil 365 366 // Wait for the SCP connection to close, meaning it has consumed all 367 // our data and has completed. Or has errored. 368 log.Println("Waiting for SSH session to complete.") 369 err = session.Wait() 370 if err != nil { 371 if exitErr, ok := err.(*ssh.ExitError); ok { 372 // Otherwise, we have an ExitErorr, meaning we can just read 373 // the exit status 374 log.Printf("non-zero exit status: %d", exitErr.ExitStatus()) 375 376 // If we exited with status 127, it means SCP isn't available. 377 // Return a more descriptive error for that. 378 if exitErr.ExitStatus() == 127 { 379 return errors.New( 380 "SCP failed to start. This usually means that SCP is not\n" + 381 "properly installed on the remote system.") 382 } 383 } 384 385 return err 386 } 387 388 log.Printf("scp stderr (length %d): %s", stderr.Len(), stderr.String()) 389 return nil 390 } 391 392 // checkSCPStatus checks that a prior command sent to SCP completed 393 // successfully. If it did not complete successfully, an error will 394 // be returned. 395 func checkSCPStatus(r *bufio.Reader) error { 396 code, err := r.ReadByte() 397 if err != nil { 398 return err 399 } 400 401 if code != 0 { 402 // Treat any non-zero (really 1 and 2) as fatal errors 403 message, _, err := r.ReadLine() 404 if err != nil { 405 return fmt.Errorf("Error reading error message: %s", err) 406 } 407 408 return errors.New(string(message)) 409 } 410 411 return nil 412 } 413 414 func scpUploadFile(dst string, src io.Reader, w io.Writer, r *bufio.Reader) error { 415 // Create a temporary file where we can copy the contents of the src 416 // so that we can determine the length, since SCP is length-prefixed. 417 tf, err := ioutil.TempFile("", "terraform-upload") 418 if err != nil { 419 return fmt.Errorf("Error creating temporary file for upload: %s", err) 420 } 421 defer os.Remove(tf.Name()) 422 defer tf.Close() 423 424 log.Println("Copying input data into temporary file so we can read the length") 425 if _, err := io.Copy(tf, src); err != nil { 426 return err 427 } 428 429 // Sync the file so that the contents are definitely on disk, then 430 // read the length of it. 431 if err := tf.Sync(); err != nil { 432 return fmt.Errorf("Error creating temporary file for upload: %s", err) 433 } 434 435 // Seek the file to the beginning so we can re-read all of it 436 if _, err := tf.Seek(0, 0); err != nil { 437 return fmt.Errorf("Error creating temporary file for upload: %s", err) 438 } 439 440 fi, err := tf.Stat() 441 if err != nil { 442 return fmt.Errorf("Error creating temporary file for upload: %s", err) 443 } 444 445 // Start the protocol 446 log.Println("Beginning file upload...") 447 fmt.Fprintln(w, "C0644", fi.Size(), dst) 448 if err := checkSCPStatus(r); err != nil { 449 return err 450 } 451 452 if _, err := io.Copy(w, tf); err != nil { 453 return err 454 } 455 456 fmt.Fprint(w, "\x00") 457 if err := checkSCPStatus(r); err != nil { 458 return err 459 } 460 461 return nil 462 } 463 464 func scpUploadDirProtocol(name string, w io.Writer, r *bufio.Reader, f func() error) error { 465 log.Printf("SCP: starting directory upload: %s", name) 466 fmt.Fprintln(w, "D0755 0", name) 467 err := checkSCPStatus(r) 468 if err != nil { 469 return err 470 } 471 472 if err := f(); err != nil { 473 return err 474 } 475 476 fmt.Fprintln(w, "E") 477 if err != nil { 478 return err 479 } 480 481 return nil 482 } 483 484 func scpUploadDir(root string, fs []os.FileInfo, w io.Writer, r *bufio.Reader) error { 485 for _, fi := range fs { 486 realPath := filepath.Join(root, fi.Name()) 487 488 // Track if this is actually a symlink to a directory. If it is 489 // a symlink to a file we don't do any special behavior because uploading 490 // a file just works. If it is a directory, we need to know so we 491 // treat it as such. 492 isSymlinkToDir := false 493 if fi.Mode()&os.ModeSymlink == os.ModeSymlink { 494 symPath, err := filepath.EvalSymlinks(realPath) 495 if err != nil { 496 return err 497 } 498 499 symFi, err := os.Lstat(symPath) 500 if err != nil { 501 return err 502 } 503 504 isSymlinkToDir = symFi.IsDir() 505 } 506 507 if !fi.IsDir() && !isSymlinkToDir { 508 // It is a regular file (or symlink to a file), just upload it 509 f, err := os.Open(realPath) 510 if err != nil { 511 return err 512 } 513 514 err = func() error { 515 defer f.Close() 516 return scpUploadFile(fi.Name(), f, w, r) 517 }() 518 519 if err != nil { 520 return err 521 } 522 523 continue 524 } 525 526 // It is a directory, recursively upload 527 err := scpUploadDirProtocol(fi.Name(), w, r, func() error { 528 f, err := os.Open(realPath) 529 if err != nil { 530 return err 531 } 532 defer f.Close() 533 534 entries, err := f.Readdir(-1) 535 if err != nil { 536 return err 537 } 538 539 return scpUploadDir(realPath, entries, w, r) 540 }) 541 if err != nil { 542 return err 543 } 544 } 545 546 return nil 547 } 548 549 // ConnectFunc is a convenience method for returning a function 550 // that just uses net.Dial to communicate with the remote end that 551 // is suitable for use with the SSH communicator configuration. 552 func ConnectFunc(network, addr string) func() (net.Conn, error) { 553 return func() (net.Conn, error) { 554 c, err := net.DialTimeout(network, addr, 15*time.Second) 555 if err != nil { 556 return nil, err 557 } 558 559 if tcpConn, ok := c.(*net.TCPConn); ok { 560 tcpConn.SetKeepAlive(true) 561 } 562 563 return c, nil 564 } 565 }