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