2015-06-13 17:42:38 -04:00
|
|
|
package communicator
|
|
|
|
|
|
|
|
import (
|
2018-01-22 18:32:33 -05:00
|
|
|
"context"
|
2015-06-13 17:42:38 -04:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2020-03-13 01:01:11 -04:00
|
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
|
|
"io"
|
2015-06-13 17:42:38 -04:00
|
|
|
"log"
|
2015-06-17 16:10:42 -04:00
|
|
|
"net"
|
2017-05-28 08:05:03 -04:00
|
|
|
"os"
|
2015-06-13 17:42:38 -04:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2017-04-04 16:39:01 -04:00
|
|
|
"github.com/hashicorp/packer/communicator/ssh"
|
2018-01-19 19:18:44 -05:00
|
|
|
"github.com/hashicorp/packer/helper/multistep"
|
2018-08-27 10:13:15 -04:00
|
|
|
helperssh "github.com/hashicorp/packer/helper/ssh"
|
2017-04-04 16:39:01 -04:00
|
|
|
"github.com/hashicorp/packer/packer"
|
2015-06-13 17:42:38 -04:00
|
|
|
gossh "golang.org/x/crypto/ssh"
|
2017-05-28 08:05:03 -04:00
|
|
|
"golang.org/x/crypto/ssh/agent"
|
2017-10-10 10:04:15 -04:00
|
|
|
"golang.org/x/net/proxy"
|
2015-06-13 17:42:38 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// StepConnectSSH is a step that only connects to SSH.
|
|
|
|
//
|
|
|
|
// In general, you should use StepConnect.
|
|
|
|
type StepConnectSSH struct {
|
|
|
|
// All the fields below are documented on StepConnect
|
2015-06-13 19:23:33 -04:00
|
|
|
Config *Config
|
|
|
|
Host func(multistep.StateBag) (string, error)
|
|
|
|
SSHConfig func(multistep.StateBag) (*gossh.ClientConfig, error)
|
2019-03-19 09:47:21 -04:00
|
|
|
SSHPort func(multistep.StateBag) (int, error)
|
2015-06-13 17:42:38 -04:00
|
|
|
}
|
|
|
|
|
2019-03-29 11:50:02 -04:00
|
|
|
func (s *StepConnectSSH) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
2015-06-13 17:42:38 -04:00
|
|
|
ui := state.Get("ui").(packer.Ui)
|
|
|
|
|
|
|
|
var comm packer.Communicator
|
|
|
|
var err error
|
|
|
|
|
2019-07-12 05:54:43 -04:00
|
|
|
subCtx, cancel := context.WithCancel(ctx)
|
2015-06-13 17:42:38 -04:00
|
|
|
waitDone := make(chan bool, 1)
|
|
|
|
go func() {
|
|
|
|
ui.Say("Waiting for SSH to become available...")
|
2019-07-12 05:54:43 -04:00
|
|
|
comm, err = s.waitForSSH(state, subCtx)
|
|
|
|
cancel() // just to make 'possible context leak' analysis happy
|
2015-06-13 17:42:38 -04:00
|
|
|
waitDone <- true
|
|
|
|
}()
|
|
|
|
|
|
|
|
log.Printf("[INFO] Waiting for SSH, up to timeout: %s", s.Config.SSHTimeout)
|
|
|
|
timeout := time.After(s.Config.SSHTimeout)
|
|
|
|
for {
|
|
|
|
// Wait for either SSH to become available, a timeout to occur,
|
|
|
|
// or an interrupt to come through.
|
|
|
|
select {
|
|
|
|
case <-waitDone:
|
|
|
|
if err != nil {
|
|
|
|
ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err))
|
|
|
|
state.Put("error", err)
|
|
|
|
return multistep.ActionHalt
|
|
|
|
}
|
|
|
|
|
|
|
|
ui.Say("Connected to SSH!")
|
|
|
|
state.Put("communicator", comm)
|
2019-07-12 05:54:43 -04:00
|
|
|
return multistep.ActionContinue
|
2015-06-13 17:42:38 -04:00
|
|
|
case <-timeout:
|
|
|
|
err := fmt.Errorf("Timeout waiting for SSH.")
|
|
|
|
state.Put("error", err)
|
|
|
|
ui.Error(err.Error())
|
2019-07-12 05:54:43 -04:00
|
|
|
cancel()
|
|
|
|
return multistep.ActionHalt
|
|
|
|
case <-ctx.Done():
|
|
|
|
// The step sequence was cancelled, so cancel waiting for SSH
|
|
|
|
// and just start the halting process.
|
|
|
|
cancel()
|
|
|
|
log.Println("[WARN] Interrupt detected, quitting waiting for SSH.")
|
2015-06-13 17:42:38 -04:00
|
|
|
return multistep.ActionHalt
|
|
|
|
case <-time.After(1 * time.Second):
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *StepConnectSSH) Cleanup(multistep.StateBag) {
|
|
|
|
}
|
|
|
|
|
2019-07-12 05:54:43 -04:00
|
|
|
func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, ctx context.Context) (packer.Communicator, error) {
|
2015-06-17 16:10:42 -04:00
|
|
|
// Determine if we're using a bastion host, and if so, retrieve
|
|
|
|
// that configuration. This configuration doesn't change so we
|
|
|
|
// do this one before entering the retry loop.
|
|
|
|
var bProto, bAddr string
|
|
|
|
var bConf *gossh.ClientConfig
|
2017-10-10 10:04:15 -04:00
|
|
|
var pAddr string
|
|
|
|
var pAuth *proxy.Auth
|
2015-06-17 16:10:42 -04:00
|
|
|
if s.Config.SSHBastionHost != "" {
|
|
|
|
// The protocol is hardcoded for now, but may be configurable one day
|
|
|
|
bProto = "tcp"
|
|
|
|
bAddr = fmt.Sprintf(
|
|
|
|
"%s:%d", s.Config.SSHBastionHost, s.Config.SSHBastionPort)
|
|
|
|
|
|
|
|
conf, err := sshBastionConfig(s.Config)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Error configuring bastion: %s", err)
|
|
|
|
}
|
|
|
|
bConf = conf
|
|
|
|
}
|
|
|
|
|
2017-10-10 10:04:15 -04:00
|
|
|
if s.Config.SSHProxyHost != "" {
|
|
|
|
pAddr = fmt.Sprintf("%s:%d", s.Config.SSHProxyHost, s.Config.SSHProxyPort)
|
|
|
|
if s.Config.SSHProxyUsername != "" {
|
|
|
|
pAuth = new(proxy.Auth)
|
2019-11-15 15:08:05 -05:00
|
|
|
pAuth.User = s.Config.SSHProxyUsername
|
|
|
|
pAuth.Password = s.Config.SSHProxyPassword
|
2017-10-10 10:04:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2015-06-13 17:42:38 -04:00
|
|
|
handshakeAttempts := 0
|
|
|
|
|
|
|
|
var comm packer.Communicator
|
|
|
|
first := true
|
|
|
|
for {
|
|
|
|
// Don't check for cancel or wait on first iteration
|
|
|
|
if !first {
|
|
|
|
select {
|
2019-07-12 05:54:43 -04:00
|
|
|
case <-ctx.Done():
|
2015-06-13 17:42:38 -04:00
|
|
|
log.Println("[DEBUG] SSH wait cancelled. Exiting loop.")
|
|
|
|
return nil, errors.New("SSH wait cancelled")
|
|
|
|
case <-time.After(5 * time.Second):
|
|
|
|
}
|
|
|
|
}
|
|
|
|
first = false
|
|
|
|
|
|
|
|
// First we request the TCP connection information
|
2015-06-13 19:23:33 -04:00
|
|
|
host, err := s.Host(state)
|
2015-06-13 17:42:38 -04:00
|
|
|
if err != nil {
|
|
|
|
log.Printf("[DEBUG] Error getting SSH address: %s", err)
|
|
|
|
continue
|
|
|
|
}
|
2019-10-04 14:36:57 -04:00
|
|
|
// store host and port in config so we can access them from provisioners
|
|
|
|
s.Config.SSHHost = host
|
2015-06-13 19:23:33 -04:00
|
|
|
port := s.Config.SSHPort
|
|
|
|
if s.SSHPort != nil {
|
|
|
|
port, err = s.SSHPort(state)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[DEBUG] Error getting SSH port: %s", err)
|
|
|
|
continue
|
|
|
|
}
|
2019-10-04 14:36:57 -04:00
|
|
|
s.Config.SSHPort = port
|
2015-06-13 19:23:33 -04:00
|
|
|
}
|
2019-10-04 14:36:57 -04:00
|
|
|
state.Put("communicator_config", s.Config)
|
2015-06-13 17:42:38 -04:00
|
|
|
|
|
|
|
// Retrieve the SSH configuration
|
|
|
|
sshConfig, err := s.SSHConfig(state)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[DEBUG] Error getting SSH config: %s", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2015-06-17 16:10:42 -04:00
|
|
|
// Attempt to connect to SSH port
|
|
|
|
var connFunc func() (net.Conn, error)
|
2015-06-13 19:23:33 -04:00
|
|
|
address := fmt.Sprintf("%s:%d", host, port)
|
2015-06-17 16:10:42 -04:00
|
|
|
if bAddr != "" {
|
|
|
|
// We're using a bastion host, so use the bastion connfunc
|
|
|
|
connFunc = ssh.BastionConnectFunc(
|
|
|
|
bProto, bAddr, bConf, "tcp", address)
|
2017-10-10 10:04:15 -04:00
|
|
|
} else if pAddr != "" {
|
|
|
|
// Connect via SOCKS5 proxy
|
|
|
|
connFunc = ssh.ProxyConnectFunc(pAddr, pAuth, "tcp", address)
|
2015-06-17 16:10:42 -04:00
|
|
|
} else {
|
|
|
|
// No bastion host, connect directly
|
|
|
|
connFunc = ssh.ConnectFunc("tcp", address)
|
|
|
|
}
|
2015-06-13 19:23:33 -04:00
|
|
|
|
2015-06-13 17:42:38 -04:00
|
|
|
nc, err := connFunc()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[DEBUG] TCP connection to SSH ip/port failed: %s", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
nc.Close()
|
|
|
|
|
2019-07-26 17:11:52 -04:00
|
|
|
// Parse out all the requested Port Tunnels that will go over our SSH connection
|
|
|
|
var tunnels []ssh.TunnelSpec
|
|
|
|
for _, v := range s.Config.SSHLocalTunnels {
|
|
|
|
t, err := helperssh.ParseTunnelArgument(v, ssh.LocalTunnel)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"Error parsing port forwarding: %s", err)
|
|
|
|
}
|
|
|
|
tunnels = append(tunnels, t)
|
|
|
|
}
|
|
|
|
for _, v := range s.Config.SSHRemoteTunnels {
|
|
|
|
t, err := helperssh.ParseTunnelArgument(v, ssh.RemoteTunnel)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"Error parsing port forwarding: %s", err)
|
|
|
|
}
|
|
|
|
tunnels = append(tunnels, t)
|
|
|
|
}
|
|
|
|
|
2015-06-13 17:42:38 -04:00
|
|
|
// Then we attempt to connect via SSH
|
|
|
|
config := &ssh.Config{
|
2018-12-02 20:42:34 -05:00
|
|
|
Connection: connFunc,
|
|
|
|
SSHConfig: sshConfig,
|
|
|
|
Pty: s.Config.SSHPty,
|
2017-06-19 10:21:33 -04:00
|
|
|
DisableAgentForwarding: s.Config.SSHDisableAgentForwarding,
|
|
|
|
UseSftp: s.Config.SSHFileTransferMethod == "sftp",
|
2018-01-31 01:00:37 -05:00
|
|
|
KeepAliveInterval: s.Config.SSHKeepAliveInterval,
|
2018-01-31 02:09:12 -05:00
|
|
|
Timeout: s.Config.SSHReadWriteTimeout,
|
2019-07-26 17:11:52 -04:00
|
|
|
Tunnels: tunnels,
|
2015-06-13 17:42:38 -04:00
|
|
|
}
|
|
|
|
|
2019-03-19 10:32:43 -04:00
|
|
|
log.Printf("[INFO] Attempting SSH connection to %s...", address)
|
2015-06-13 17:42:38 -04:00
|
|
|
comm, err = ssh.New(address, config)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[DEBUG] SSH handshake err: %s", err)
|
|
|
|
|
|
|
|
// Only count this as an attempt if we were able to attempt
|
|
|
|
// to authenticate. Note this is very brittle since it depends
|
|
|
|
// on the string of the error... but I don't see any other way.
|
|
|
|
if strings.Contains(err.Error(), "authenticate") {
|
|
|
|
log.Printf(
|
|
|
|
"[DEBUG] Detected authentication error. Increasing handshake attempts.")
|
2019-09-27 14:12:53 -04:00
|
|
|
err = fmt.Errorf("Packer experienced an authentication error "+
|
|
|
|
"when trying to connect via SSH. This can happen if your "+
|
|
|
|
"username/password are wrong. You may want to double-check"+
|
|
|
|
" your credentials as part of your debugging process. "+
|
|
|
|
"original error: %s",
|
|
|
|
err)
|
2015-06-13 17:42:38 -04:00
|
|
|
handshakeAttempts += 1
|
|
|
|
}
|
|
|
|
|
2015-06-13 19:39:42 -04:00
|
|
|
if handshakeAttempts < s.Config.SSHHandshakeAttempts {
|
|
|
|
// Try to connect via SSH a handful of times. We sleep here
|
|
|
|
// so we don't get a ton of authentication errors back to back.
|
|
|
|
time.Sleep(2 * time.Second)
|
2015-06-13 17:42:38 -04:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
return comm, nil
|
|
|
|
}
|
2015-06-17 16:10:42 -04:00
|
|
|
|
|
|
|
func sshBastionConfig(config *Config) (*gossh.ClientConfig, error) {
|
2017-02-27 14:30:08 -05:00
|
|
|
auth := make([]gossh.AuthMethod, 0, 2)
|
2020-03-05 22:22:48 -05:00
|
|
|
|
2020-03-10 21:37:57 -04:00
|
|
|
if config.SSHBastionInteractive {
|
2020-03-13 01:01:11 -04:00
|
|
|
var c io.ReadWriteCloser
|
|
|
|
if terminal.IsTerminal(int(os.Stdin.Fd())) {
|
|
|
|
c = os.Stdin
|
|
|
|
} else {
|
|
|
|
tty, err := os.Open("/dev/tty")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer tty.Close()
|
|
|
|
c = tty
|
|
|
|
}
|
|
|
|
auth = append(auth, gossh.KeyboardInteractive(ssh.KeyboardInteractive(c)))
|
2020-03-10 21:37:57 -04:00
|
|
|
}
|
2020-03-05 22:22:48 -05:00
|
|
|
|
2015-06-17 16:10:42 -04:00
|
|
|
if config.SSHBastionPassword != "" {
|
|
|
|
auth = append(auth,
|
|
|
|
gossh.Password(config.SSHBastionPassword),
|
|
|
|
gossh.KeyboardInteractive(
|
|
|
|
ssh.PasswordKeyboardInteractive(config.SSHBastionPassword)))
|
|
|
|
}
|
|
|
|
|
2018-08-27 10:33:30 -04:00
|
|
|
if config.SSHBastionPrivateKeyFile != "" {
|
2018-12-02 19:56:33 -05:00
|
|
|
path, err := packer.ExpandUser(config.SSHBastionPrivateKeyFile)
|
2018-10-31 18:26:18 -04:00
|
|
|
if err != nil {
|
2018-12-02 19:32:20 -05:00
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"Error expanding path for SSH bastion private key: %s", err)
|
2018-10-31 18:26:18 -04:00
|
|
|
}
|
2018-12-02 18:30:01 -05:00
|
|
|
|
2018-10-31 18:26:18 -04:00
|
|
|
signer, err := helperssh.FileSigner(path)
|
2015-06-17 16:10:42 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
auth = append(auth, gossh.PublicKeys(signer))
|
|
|
|
}
|
|
|
|
|
2017-05-28 08:05:03 -04:00
|
|
|
if config.SSHBastionAgentAuth {
|
|
|
|
authSock := os.Getenv("SSH_AUTH_SOCK")
|
|
|
|
if authSock == "" {
|
|
|
|
return nil, fmt.Errorf("SSH_AUTH_SOCK is not set")
|
|
|
|
}
|
|
|
|
|
|
|
|
sshAgent, err := net.Dial("unix", authSock)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Cannot connect to SSH Agent socket %q: %s", authSock, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
auth = append(auth, gossh.PublicKeysCallback(agent.NewClient(sshAgent).Signers))
|
|
|
|
}
|
|
|
|
|
2015-06-17 16:10:42 -04:00
|
|
|
return &gossh.ClientConfig{
|
2017-05-28 14:35:01 -04:00
|
|
|
User: config.SSHBastionUsername,
|
|
|
|
Auth: auth,
|
|
|
|
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
|
2015-06-17 16:10:42 -04:00
|
|
|
}, nil
|
|
|
|
}
|