github.com/markdia/terraform@v0.5.1-0.20150508012022-f1ae920aa970/communicator/ssh/provisioner.go (about)

     1  package ssh
     2  
     3  import (
     4  	"encoding/pem"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"net"
     9  	"os"
    10  	"time"
    11  
    12  	"github.com/hashicorp/terraform/terraform"
    13  	"github.com/mitchellh/go-homedir"
    14  	"github.com/mitchellh/mapstructure"
    15  	"golang.org/x/crypto/ssh"
    16  	"golang.org/x/crypto/ssh/agent"
    17  )
    18  
    19  const (
    20  	// DefaultUser is used if there is no user given
    21  	DefaultUser = "root"
    22  
    23  	// DefaultPort is used if there is no port given
    24  	DefaultPort = 22
    25  
    26  	// DefaultScriptPath is used as the path to copy the file to
    27  	// for remote execution if not provided otherwise.
    28  	DefaultScriptPath = "/tmp/terraform_%RAND%.sh"
    29  
    30  	// DefaultTimeout is used if there is no timeout given
    31  	DefaultTimeout = 5 * time.Minute
    32  )
    33  
    34  // connectionInfo is decoded from the ConnInfo of the resource. These are the
    35  // only keys we look at. If a KeyFile is given, that is used instead
    36  // of a password.
    37  type connectionInfo struct {
    38  	User       string
    39  	Password   string
    40  	KeyFile    string `mapstructure:"key_file"`
    41  	Host       string
    42  	Port       int
    43  	Agent      bool
    44  	Timeout    string
    45  	ScriptPath string        `mapstructure:"script_path"`
    46  	TimeoutVal time.Duration `mapstructure:"-"`
    47  }
    48  
    49  // parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
    50  // a ConnectionInfo struct
    51  func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
    52  	connInfo := &connectionInfo{}
    53  	decConf := &mapstructure.DecoderConfig{
    54  		WeaklyTypedInput: true,
    55  		Result:           connInfo,
    56  	}
    57  	dec, err := mapstructure.NewDecoder(decConf)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	if connInfo.User == "" {
    66  		connInfo.User = DefaultUser
    67  	}
    68  	if connInfo.Port == 0 {
    69  		connInfo.Port = DefaultPort
    70  	}
    71  	if connInfo.ScriptPath == "" {
    72  		connInfo.ScriptPath = DefaultScriptPath
    73  	}
    74  	if connInfo.Timeout != "" {
    75  		connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
    76  	} else {
    77  		connInfo.TimeoutVal = DefaultTimeout
    78  	}
    79  
    80  	return connInfo, nil
    81  }
    82  
    83  // safeDuration returns either the parsed duration or a default value
    84  func safeDuration(dur string, defaultDur time.Duration) time.Duration {
    85  	d, err := time.ParseDuration(dur)
    86  	if err != nil {
    87  		log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
    88  		return defaultDur
    89  	}
    90  	return d
    91  }
    92  
    93  // prepareSSHConfig is used to turn the *ConnectionInfo provided into a
    94  // usable *SSHConfig for client initialization.
    95  func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
    96  	var conn net.Conn
    97  	var err error
    98  
    99  	sshConf := &ssh.ClientConfig{
   100  		User: connInfo.User,
   101  	}
   102  	if connInfo.Agent {
   103  		sshAuthSock := os.Getenv("SSH_AUTH_SOCK")
   104  
   105  		if sshAuthSock == "" {
   106  			return nil, fmt.Errorf("SSH Requested but SSH_AUTH_SOCK not-specified")
   107  		}
   108  
   109  		conn, err = net.Dial("unix", sshAuthSock)
   110  		if err != nil {
   111  			return nil, fmt.Errorf("Error connecting to SSH_AUTH_SOCK: %v", err)
   112  		}
   113  		// I need to close this but, later after all connections have been made
   114  		// defer conn.Close()
   115  		signers, err := agent.NewClient(conn).Signers()
   116  		if err != nil {
   117  			return nil, fmt.Errorf("Error getting keys from ssh agent: %v", err)
   118  		}
   119  
   120  		sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signers...))
   121  	}
   122  	if connInfo.KeyFile != "" {
   123  		fullPath, err := homedir.Expand(connInfo.KeyFile)
   124  		if err != nil {
   125  			return nil, fmt.Errorf("Failed to expand home directory: %v", err)
   126  		}
   127  		key, err := ioutil.ReadFile(fullPath)
   128  		if err != nil {
   129  			return nil, fmt.Errorf("Failed to read key file '%s': %v", connInfo.KeyFile, err)
   130  		}
   131  
   132  		// We parse the private key on our own first so that we can
   133  		// show a nicer error if the private key has a password.
   134  		block, _ := pem.Decode(key)
   135  		if block == nil {
   136  			return nil, fmt.Errorf(
   137  				"Failed to read key '%s': no key found", connInfo.KeyFile)
   138  		}
   139  		if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
   140  			return nil, fmt.Errorf(
   141  				"Failed to read key '%s': password protected keys are\n"+
   142  					"not supported. Please decrypt the key prior to use.", connInfo.KeyFile)
   143  		}
   144  
   145  		signer, err := ssh.ParsePrivateKey(key)
   146  		if err != nil {
   147  			return nil, fmt.Errorf("Failed to parse key file '%s': %v", connInfo.KeyFile, err)
   148  		}
   149  
   150  		sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signer))
   151  	}
   152  	if connInfo.Password != "" {
   153  		sshConf.Auth = append(sshConf.Auth,
   154  			ssh.Password(connInfo.Password))
   155  		sshConf.Auth = append(sshConf.Auth,
   156  			ssh.KeyboardInteractive(PasswordKeyboardInteractive(connInfo.Password)))
   157  	}
   158  	host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
   159  	config := &sshConfig{
   160  		config:       sshConf,
   161  		connection:   ConnectFunc("tcp", host),
   162  		sshAgentConn: conn,
   163  	}
   164  	return config, nil
   165  }