terraform/communicator/ssh/provisioner.go

276 lines
7.0 KiB
Go

package ssh
import (
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"time"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-homedir"
"github.com/mitchellh/mapstructure"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
const (
// DefaultUser is used if there is no user given
DefaultUser = "root"
// DefaultPort is used if there is no port given
DefaultPort = 22
// DefaultScriptPath is used as the path to copy the file to
// for remote execution if not provided otherwise.
DefaultScriptPath = "/tmp/terraform_%RAND%.sh"
// DefaultTimeout is used if there is no timeout given
DefaultTimeout = 5 * time.Minute
)
// connectionInfo is decoded from the ConnInfo of the resource. These are the
// only keys we look at. If a KeyFile is given, that is used instead
// of a password.
type connectionInfo struct {
User string
Password string
KeyFile string `mapstructure:"key_file"`
Host string
Port int
Agent bool
Timeout string
ScriptPath string `mapstructure:"script_path"`
TimeoutVal time.Duration `mapstructure:"-"`
BastionUser string `mapstructure:"bastion_user"`
BastionPassword string `mapstructure:"bastion_password"`
BastionKeyFile string `mapstructure:"bastion_key_file"`
BastionHost string `mapstructure:"bastion_host"`
BastionPort int `mapstructure:"bastion_port"`
}
// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
// a ConnectionInfo struct
func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
connInfo := &connectionInfo{}
decConf := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: connInfo,
}
dec, err := mapstructure.NewDecoder(decConf)
if err != nil {
return nil, err
}
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
return nil, err
}
// To default Agent to true, we need to check the raw string, since the
// decoded boolean can't represent "absence of config".
//
// And if SSH_AUTH_SOCK is not set, there's no agent to connect to, so we
// shouldn't try.
if s.Ephemeral.ConnInfo["agent"] == "" && os.Getenv("SSH_AUTH_SOCK") != "" {
connInfo.Agent = true
}
if connInfo.User == "" {
connInfo.User = DefaultUser
}
if connInfo.Port == 0 {
connInfo.Port = DefaultPort
}
if connInfo.ScriptPath == "" {
connInfo.ScriptPath = DefaultScriptPath
}
if connInfo.Timeout != "" {
connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
} else {
connInfo.TimeoutVal = DefaultTimeout
}
// Default all bastion config attrs to their non-bastion counterparts
if connInfo.BastionHost != "" {
if connInfo.BastionUser == "" {
connInfo.BastionUser = connInfo.User
}
if connInfo.BastionPassword == "" {
connInfo.BastionPassword = connInfo.Password
}
if connInfo.BastionKeyFile == "" {
connInfo.BastionKeyFile = connInfo.KeyFile
}
if connInfo.BastionPort == 0 {
connInfo.BastionPort = connInfo.Port
}
}
return connInfo, nil
}
// safeDuration returns either the parsed duration or a default value
func safeDuration(dur string, defaultDur time.Duration) time.Duration {
d, err := time.ParseDuration(dur)
if err != nil {
log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
return defaultDur
}
return d
}
// prepareSSHConfig is used to turn the *ConnectionInfo provided into a
// usable *SSHConfig for client initialization.
func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
sshAgent, err := connectToAgent(connInfo)
if err != nil {
return nil, err
}
sshConf, err := buildSSHClientConfig(sshClientConfigOpts{
user: connInfo.User,
keyFile: connInfo.KeyFile,
password: connInfo.Password,
sshAgent: sshAgent,
})
if err != nil {
return nil, err
}
var bastionConf *ssh.ClientConfig
if connInfo.BastionHost != "" {
bastionConf, err = buildSSHClientConfig(sshClientConfigOpts{
user: connInfo.BastionUser,
keyFile: connInfo.BastionKeyFile,
password: connInfo.BastionPassword,
sshAgent: sshAgent,
})
if err != nil {
return nil, err
}
}
host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
connectFunc := ConnectFunc("tcp", host)
if bastionConf != nil {
bastionHost := fmt.Sprintf("%s:%d", connInfo.BastionHost, connInfo.BastionPort)
connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host)
}
config := &sshConfig{
config: sshConf,
connection: connectFunc,
sshAgent: sshAgent,
}
return config, nil
}
type sshClientConfigOpts struct {
keyFile string
password string
sshAgent *sshAgent
user string
}
func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) {
conf := &ssh.ClientConfig{
User: opts.user,
}
if opts.keyFile != "" {
pubKeyAuth, err := readPublicKeyFromPath(opts.keyFile)
if err != nil {
return nil, err
}
conf.Auth = append(conf.Auth, pubKeyAuth)
}
if opts.password != "" {
conf.Auth = append(conf.Auth, ssh.Password(opts.password))
conf.Auth = append(conf.Auth, ssh.KeyboardInteractive(
PasswordKeyboardInteractive(opts.password)))
}
if opts.sshAgent != nil {
conf.Auth = append(conf.Auth, opts.sshAgent.Auth())
}
return conf, nil
}
func readPublicKeyFromPath(path string) (ssh.AuthMethod, error) {
fullPath, err := homedir.Expand(path)
if err != nil {
return nil, fmt.Errorf("Failed to expand home directory: %s", err)
}
key, err := ioutil.ReadFile(fullPath)
if err != nil {
return nil, fmt.Errorf("Failed to read key file %q: %s", path, err)
}
// We parse the private key on our own first so that we can
// show a nicer error if the private key has a password.
block, _ := pem.Decode(key)
if block == nil {
return nil, fmt.Errorf("Failed to read key %q: no key found", path)
}
if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
return nil, fmt.Errorf(
"Failed to read key %q: password protected keys are\n"+
"not supported. Please decrypt the key prior to use.", path)
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return nil, fmt.Errorf("Failed to parse key file %q: %s", path, err)
}
return ssh.PublicKeys(signer), nil
}
func connectToAgent(connInfo *connectionInfo) (*sshAgent, error) {
if connInfo.Agent != true {
// No agent configured
return nil, nil
}
sshAuthSock := os.Getenv("SSH_AUTH_SOCK")
if sshAuthSock == "" {
return nil, fmt.Errorf("SSH Requested but SSH_AUTH_SOCK not-specified")
}
conn, err := net.Dial("unix", sshAuthSock)
if err != nil {
return nil, fmt.Errorf("Error connecting to SSH_AUTH_SOCK: %v", err)
}
// connection close is handled over in Communicator
return &sshAgent{
agent: agent.NewClient(conn),
conn: conn,
}, nil
}
// A tiny wrapper around an agent.Agent to expose the ability to close its
// associated connection on request.
type sshAgent struct {
agent agent.Agent
conn net.Conn
}
func (a *sshAgent) Close() error {
return a.conn.Close()
}
func (a *sshAgent) Auth() ssh.AuthMethod {
return ssh.PublicKeysCallback(a.agent.Signers)
}
func (a *sshAgent) ForwardToAgent(client *ssh.Client) error {
return agent.ForwardToAgent(client, a.agent)
}