github.com/alexissmirnov/terraform@v0.4.3-0.20150423153700-1ef9731a2f14/helper/ssh/provisioner.go (about)

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