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